在单独使用 MyBatis 进行数据库操作时,我们通常都会先调用 SqlSession 接口的getMapper方法为我们的Mapper接口生成实现类。然后就可以通过Mapper进行数据库操作。比如像下面这样:
1 2
| ArticleMapper articleMapper = session.getMapper(ArticleMapper.class); Article article = articleMapper.findOne(1);
|
SqlSession 是通过JDK动态代理的方式为接口生成代理对象的。在调用接口方法时,相关调用会被代理逻辑拦截。在代理逻辑中可根据方法名及方法归属接口获取到当前方法对应的 SQL 以及其他一些信息,拿到这些信息即可进行数据库操作。
为 Mapper 接口创建代理对象
我们从 DefaultSqlSession 的 getMapper 方法开始看起
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| // -☆- DefaultSqlSession public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); }
// -☆- Configuration public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }
// -☆- MapperRegistry public <T> T getMapper(Class<T> type, SqlSession sqlSession) { // 从 knownMappers 中获取与 type 对应的 MapperProxyFactory final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("……"); } try { // 创建代理对象 return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("……"); } }
|
MyBatis 在解析配置文件的节点的过程中,会调用MapperRegistry 的 addMapper 方法将 Class 到 MapperProxyFactory 对象的映射关系存入到knownMappers。
在获取到 MapperProxyFactory 对象后,即可调用工厂方法为 Mapper 接口生成代理对象了。相关逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| // -☆- MapperProxyFactory public T newInstance(SqlSession sqlSession) { // 创建 MapperProxy 对象,MapperProxy 实现了 InvocationHandler 接口, // 代理逻辑封装在此类中 final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }
protected T newInstance(MapperProxy<T> mapperProxy) { // 通过 JDK 动态代理创建代理对象 return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy); }
|
首先创建了一个 MapperProxy 对象,该对象实现了 InvocationHandler 接口。然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成代理对象。代理对象已经创建完毕,下面就可以调用接口方法进行数据库操作了。
执⾏代理逻辑
Mapper 接口方法的代理逻辑首先会对拦截的方法进行一些检测,以决定是否执行后续的数据库操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 如果方法是定义在 Object 类中的,则直接调用 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); /* * 下面的代码最早出现在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的 * 新特性 - 默认方法。这段代码的逻辑就不分析了,有兴趣的同学可以 * 去 Github 上看一下相关的相关的讨论(issue #709),链接如下: * * https://github.com/mybatis/mybatis-3/issues/709 */ } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 从缓存中获取 MapperMethod 对象,若缓存未命中,则创建 MapperMethod 对象 final MapperMethod mapperMethod = cachedMapperMethod(method); // 调用 execute 方法执行 SQL return mapperMethod.execute(sqlSession, args); }
|
代理逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、hashCode 方法等。对于这类方法,直接执行即可。
完成相关检测后,紧接着从缓存中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。
在分析execute 方法之前,我们先来看一下 MapperMethod 对象的创建过程。
创建 MapperMethod 对象
构造方法
1 2 3 4 5 6 7 8 9 10 11 12
| public class MapperMethod { private final SqlCommand command; private final MethodSignature method; public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { // 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息 this.command = new SqlCommand(config, mapperInterface, method); // 创建 MethodSignature 对象,由类名可知,该对象包含了被拦截方法的一些信息 this.method = new MethodSignature(config, mapperInterface, method); } }
|
构造方法的逻辑很简单,主要是创建 SqlCommand 和 MethodSignature 对象。这两个对象分别记录了不同的信息,这些信息在后续的方法调用中都会被用到。
创建 SqlCommand 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public static class SqlCommand { private final String name; private final SqlCommandType type; public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) { final String methodName = method.getName(); final Class<?> declaringClass = method.getDeclaringClass(); // 解析 MappedStatement MappedStatement ms = resolveMappedStatement( mapperInterface, methodName, declaringClass, configuration); // 检测当前方法是否有对应的 MappedStatement if (ms == null) { // 检测当前方法是否有 @Flush 注解 if (method.getAnnotation(Flush.class) != null) { // 设置 name 和 type 遍历 name = null; type = SqlCommandType.FLUSH; } else { // 若 ms == null 且方法无 @Flush 注解,此时抛出异常。 // 这个异常比较常见,大家应该眼熟吧 throw new BindingException("……"); } } else { // 设置 name 和 type 变量 name = ms.getId(); type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { throw new BindingException("……"); } } } }
|
主要用于初始化它的两个成员变量
创建 MethodSignature 对象
MethodSignature 即方法签名,顾名思义,该类保存了一些和目标方法相关的信息。比如目标方法的返回类型,目标方法的参数列表信息等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public static class MethodSignature { private final boolean returnsMany; private final boolean returnsMap; private final boolean returnsVoid; private final boolean returnsCursor; 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(); } // 检测返回值类型是否是 void、集合或数组、Cursor、Map 等 this.returnsVoid = void.class.equals(this.returnType); this.returnsMany = configuration.getObjectFactory() .isCollection(this.returnType) || this.returnType.isArray(); this.returnsCursor = Cursor.class.equals(this.returnType); // 解析 @MapKey 注解,获取注解内容 this.mapKey = getMapKey(method); this.returnsMap = this.mapKey != null; // 获取 RowBounds 参数在参数列表中的位置,如果参数列表中 // 包含多个 RowBounds 参数,此方法会抛出异常 this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); // 获取 ResultHandler 参数在参数列表中的位置 this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); // 解析参数列表 this.paramNameResolver = new ParamNameResolver(configuration, method); } }
|
上面的代码用于检测目标方法的返回类型,以及解析目标方法参数列表。其中,检测返回类型的目的是为避免查询方法返回错误的类型。
参数列表的解析过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| public class ParamNameResolver { private static final String GENERIC_NAME_PREFIX = "param"; private final SortedMap<Integer, String> names; 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; for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { // 检测当前的参数类型是否为 RowBounds 或 ResultHandler if (isSpecialParameter(paramTypes[paramIndex])) { continue; } String name = null; for (Annotation annotation : paramAnnotations[paramIndex]) { if (annotation instanceof Param) { hasParamAnnotation = true; // 获取 @Param 注解内容 name = ((Param) annotation).value(); break; } } // name 为空,表明未给参数配置 @Param 注解 if (name == null) { // 检测是否设置了 useActualParamName 全局配置 if (config.isUseActualParamName()) { // 通过反射获取参数名称。此种方式要求 JDK 版本为 1.8+, // 且要求编译时加入 -parameters 参数,否则获取到的参数名 // 仍然是 arg1, arg2, ..., argN name = getActualParamName(method, paramIndex); } if (name == null) { /* * 使用 map.size() 返回值作为名称,思考一下为什么不这样写: * name = String.valueOf(paramIndex); * 因为如果参数列表中包含 RowBounds 或 ResultHandler,这两个 * 参数会被忽略掉,这样将导致名称不连续。 * * 比如参数列表 (int p1, int p2, RowBounds rb, int p3) * - 期望得到名称列表为 ["0", "1", "2"] * - 实际得到名称列表为 ["0", "1", "3"] */ name = String.valueOf(map.size()); } } // 存储 paramIndex 到 name 的映射 map.put(paramIndex, name); } names = Collections.unmodifiableSortedMap(map); } }
|
方法参数列表解析完毕后,可得到参数下标与参数名的映射关系,这些映射关系最终存储在 ParamNameResolver 的 names 成员变量中。
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class ParamNameResolverTest { @Test public void test() throws NoSuchMethodException, NoSuchFieldException, IllegalAccessException { Configuration config = new Configuration(); config.setUseActualParamName(false); Method method = ArticleMapper.class.getMethod("select", Integer.class, String.class, RowBounds.class, Article.class); ParamNameResolver resolver = new ParamNameResolver(config, method); Field field = resolver.getClass().getDeclaredField("names"); field.setAccessible(true); // 通过反射获取 ParamNameResolver 私有成员变量 names Object names = field.get(resolver); System.out.println("names: " + names); } class ArticleMapper { public void select(@Param("id") Integer id, @Param("author") String author, RowBounds rb, Article article) {} } }
|
参数索引与名称映射图如下:
执⾏ execute ⽅法
现在 MapperMethod 创建好了。那么,接下来要做的事情是调用 MapperMethod 的 execute 方法,执行 SQL。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| // -☆- MapperMethod public Object execute(SqlSession sqlSession, Object[] args) { Object result; // 根据 SQL 类型执行相应的数据库操作 switch (command.getType()) { case INSERT: { // 对用户传入的参数进行转换,下同 Object param = method.convertArgsToSqlCommandParam(args); // 执行插入操作,rowCountResult 方法用于处理返回值 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,表明 // 使用者想通过 ResultHandler 的方式获取查询结果,而非通过返回值 // 获取结果 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()) { // 执行查询操作,并返回一个 Cursor 对象 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("……"); } // 如果方法的返回值为基本类型,而返回值却为 null,此种情况下应抛出异常 if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("……"); } return result; }
|
execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库操作。
convertArgsToSqlCommandParam 方法出现次数比较频繁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| // -☆- MapperMethod public Object convertArgsToSqlCommandParam(Object[] args) { return paramNameResolver.getNamedParams(args); }
public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { /* * 如果方法参数列表无 @Param 注解,且仅有一个非特别参数,则返回该 * 参数的值。比如如下方法: * List findList(RowBounds rb, String name) * names 如下: * names = {1 : "0"} * 此种情况下,返回 args[names.firstKey()],即 args[1] -> name */ 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 中 param.put(entry.getValue(), args[entry.getKey()]); // genericParamName = param + index。比如 param1, param2,... paramN final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); // 检测 names 中是否包含 genericParamName,什么情况下会包含? // 答案如下: // 使用者显式将参数名称配置为 param1,即 @Param("param1") if (!names.containsValue(genericParamName)) { // 添加 <param*, value> 到 param 中 param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
|
convertArgsToSqlCommandParam 是一个空壳方法,该方法最终调用了ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要逻辑是根据条件返回不同的结果
MyBatis 对哪些 SQL 指令提供了支持,如下:
- 查询语句:SELECT
- 更新语句:INSERT/UPDATE/DELETE
- 存储过程:CALL
在上面的列表中,我刻意对 SELECT/INSERT/UPDATE/DELETE 等指令进行了分类,分类依据指令的功能以及 MyBatis 执行这些指令的过程。这里把 SELECT 称为查询语句,INSERT/UPDATE/DELETE 等称为更新语句。