mybatis最新版本(此处以3.4.1版本为分析对象)与ibatis时代相比最大的进步就是引入repository/dao层的接口设计,即不再需要由开发者去实现重复的样板式的dao层代码了,开发者只需要定义dao层接口以及mybatis sql xml映射文件或者annotation注解,由mybatis自动实现dao层的功能,那么这个过程的原理是怎样的?底层是怎样实现的?
分析mybatis源码,发现相关实现位于org.apache.ibatis.binding包下,原理是java proxy机制,由于采用了java动态代理机制而不是cglib的类增强代理机制,因此dao层必须设计为interface而不能是class。
在org.apache.ibatis.binding包下MapperProxy对象是实现了java.lang.reflect.InvocationHandler接口的代理类,但是该包的核心功能类是MapperMethod,对代理对象的方法调用基本由MapperMethod实现,对动态代理对象的相关信息的分析在MapperMethod构造函数中即已分析完毕:
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) { this.command = new SqlCommand(config, mapperInterface, method); this.method = new MethodSignature(config, mapperInterface, method); }
可以看到,MapperMethod类只需要接受mapper接口信息和当前调用method信息(还有一个Configuration对象),就可以根据两者携带的元数据信息确定下来相当多的信息了,这些"更多的信息"将用于实际方法调用和参数传递时的执行逻辑。因此首先分析SqlCommand和MethodSignature对象的构造过程。
1 如何识别当前代理对象当前方法对应的sql command type?
(a) 找出当前调用方法的id
先识别"String statementName = mapperInterface.getName() + "." + method.getName();",如果configuration没有这个id,则
再识别"String parentStatementName = method.getDeclaringClass().getName() + "." + method.getName();"
(b) 从Configuration对象中根据方法id找出对应的MappedStatement对象
Configuration对象中在解析阶段即存入所有配置的MappedStatement对象集合,这个配置要么来源于xml,要么来源于annotation。参见XMLStatementBuilder或MapperAnnotationBuilder。
(c) 根据找到的MappedStatement对象查出sql command的name和type属性
name = ms.getId(); type = ms.getSqlCommandType();
MappedStatement对象的id和sqlCommandType属性在构造时既已传入,由XMLStatementBuilder或MapperAnnotationBuilder解析时确定:
(c1) annotation配置下的MapperAnnotationBuilder解析
SqlCommandType sqlCommandType = getSqlCommandType(method); private SqlCommandType getSqlCommandType(Method method) { Class<? extends Annotation> type = getSqlAnnotationType(method); if (type == null) { type = getSqlProviderAnnotationType(method); if (type == null) { return SqlCommandType.UNKNOWN; } if (type == SelectProvider.class) { type = Select.class; } else if (type == InsertProvider.class) { type = Insert.class; } else if (type == UpdateProvider.class) { type = Update.class; } else if (type == DeleteProvider.class) { type = Delete.class; } } return SqlCommandType.valueOf(type.getSimpleName().toUpperCase(Locale.ENGLISH)); }
getSqlAnnotationType方法会识别当前方法的以下几种注解类别:Select、Insert、Update、Delete
如果没有找到这几种常规注解,再调用getSqlProviderAnnotationType方法继续识别当前方法的另外几种注解:SelectProvider、InsertProvider、UpdateProvider、DeleteProvider
(c2) xml配置下的XMLStatementBuilder解析
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
xml的识别更简单:直接在解析阶段根据当前node的nodeName即确定:insert | update | delete | select
2 如何识别当前代理对象当前方法的传入参数和返回类型?
在确定对应sql的id和command type后,程序在运行时动态传递由上层调用者service层传入的方法参数列表,因此需要代理对象识别这些参数信息(这样才可以在之后ORM);同时需要代理对象识别当前方法的返回对象类型,比如是查询单条记录?还是查询列表?亦或类似Page的封装了分页信息的对象?所有这些信息均在MethodSignature构造方法中确定。
(a) 确定返回对象类型
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(); } 根据resolvedReturnType确定returnType,再由returnType确定以下几个返回信息: 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);
其分别对应该方法是否返回void类型、返回Collection(包括set/list/array等)类型、返回Cursor类型、返回Map类型,这些信息会决定后续调用底层SqlSession对象的哪种对应方法。
如果希望在repository/dao层就设计一个返回分页的对象,并让mybatis自动实现,那么这里是分析的切入点。详细原理和实现参见后文。
(b) 检查方法参数中有没有RowBounds类型
先遍历方法参数列表,查看是否存在唯一的RowBounds类型的参数,有的话记住该参数位置,没有的话返回null,有多个的话抛异常--不允许一个方法签名中有多个RowBounds类型参数
Integer this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class); public boolean hasRowBounds() { return rowBoundsIndex != null; } public RowBounds extractRowBounds(Object[] args) { return hasRowBounds() ? (RowBounds) args[rowBoundsIndex] : null; }
这种RowBounds类型参数被mybatis特别拎出来单独判断是有道理的,因为基于该类型的参数可以实现特定的分页查询,详细原理和实现参见后文。
(c) 检查方法参数中有没有ResultHandler类型
与RowBounds类型的检查类似--先遍历方法参数列表,查看是否存在唯一的ResultHandler类型的参数,有的话记住该参数位置,没有的话返回null,有多个的话抛异常--不允许一个方法签名中有多个ResultHandler类型参数
Integer this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class); public boolean hasResultHandler() { return resultHandlerIndex != null; } public ResultHandler extractResultHandler(Object[] args) { return hasResultHandler() ? (ResultHandler) args[resultHandlerIndex] : null; }
ResultHandler类型参数有一个最大的特点是:可以最大限度的把对返回数据集的处理权限交给开发者自由灵活的设计实现,比如分页完全可以在这个对象中实现。
3 代理对象的真正执行逻辑
在org.apache.ibatis.binding包下MapperProxy对象是实现了java.lang.reflect.InvocationHandler接口的代理类,其执行方法如下:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { 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); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
关键是最后一行逻辑:对代理对象的方法调用实际是调用mapperMethod.execute(sqlSession, args);而MapperMethod的execute方法实现逻辑如下:‘
public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { 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()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { 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()); } if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) { throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); } return result; }
依据之前在MapperMethod构造函数中根据Class<?> mapperInterface, Method method, Configuration config确定下来的SqlCommand command和MethodSignature method两个对象信息,来决定当前调用的方法:
(a) INSERT
调用sqlSession.insert(command.getName(), param)
(b) UPDATE
调用sqlSession.update(command.getName(), param)
(c) DELETE
调用sqlSession.delete(command.getName(), param)
关于DML类型操作比较单纯简单,调用逻辑也相对固定--直接调用sqlSession对象相关DML方法,因此在设计repository/dao层接口时,可以提炼出一个公共的父接口,这样可以避免所有repository/dao的重复设计。详情参见下文。
(d) SELECT
select逻辑就比较复杂了,因为你可以查询object/list/set/array/map/cursor,甚至是自定义分页page对象等,可以看到case SELECT块下也是根据不同的情况去调用不同的方法:
如果调用方法返回类型是void并且参数中存在ResultHandler类型,则调用void executeWithResultHandler(SqlSession sqlSession, Object[] args)方法
如果调用方法returnsMany,则调用<E> Object executeForMany(SqlSession sqlSession, Object[] args)方法
如果调用方法returnsMap,则调用<K, V> Map<K, V> executeForMap(SqlSession sqlSession, Object[] args)方法
如果调用方法returnsCursor,则调用<T> Cursor<T> executeForCursor(SqlSession sqlSession, Object[] args)方法
其余情况下默认调用方法返回一个object元素,则调用sqlSession.selectOne(command.getName(), param);方法
各方法内部调用sqlSession对应的select方法,此处不展开了。