MyBatis 中 Mapper 接口的使用原理

幽沉谢世事,俯默窥唐虞。这篇文章主要讲述MyBatis 中 Mapper 接口的使用原理相关的知识,希望能为你提供帮助。
MyBatis 中 Mapper 接口的使用原理MyBatis 3 推荐使用 Mapper 接口的方式来执行 xml 配置中的 SQL,用起来很方便,也很灵活。在方便之余,想了解一下这是如何实现的,之前也大致知道是通过 JDK 的动态代理做到的,但这次想知道细节。
东西越多就越复杂,所以就以一个简单的仅依赖 MyBatis 3.4.0 的 CRUD 来逐步了解 Mapper 接口的调用。
通常是通过 xml 配置文件来创建SqlSessionFactory对象,然后再获取SqlSession对象,接着获取自定义的 Mapper 接口的代理对象,最后调用接口方法,示例如下:

/** * * @author xi * @date 2018/10/01 14:12 */ public class Demo { public static void main(String[] args) throws IOException { String resource = " mybatis-config.xml" ; InputStream is = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); SqlSession sqlSession = sqlSessionFactory.openSession(); RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class); Role role = roleMapper.getRole(1L); System.out.println(role); } }

如何解析配置文件,创建工厂,获取会话皆不是本次关注的重点,直接看下面这行即可:
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);

获取自定义 Mapper 代理对象的方法位于:org.apache.ibatis.session.SqlSession#getMapper,还是个范型方法
/** * Retrieves a mapper. * @param < T> the mapper type * @param type Mapper interface class * @return a mapper bound to this SqlSession */ < T> T getMapper(Class< T> type);

实现该方法的子类有:DefaultSqlSessionSqlSessionManager,这里关注默认实现即可:org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
@Override public < T> T getMapper(Class< T> type) { return configuration.< T> getMapper(type, this); }

这里面出现了 Configuration 对象,简单来说就是包含了 xml 配置解析内容的对象,同样它也不是现在关注的重点,继续往下跟进:org.apache.ibatis.session.Configuration#getMapper
public < T> T getMapper(Class< T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); }

这里出现了MapperRegistry对象,它是解析 Mapper.xml 中的内容(mapper标签中的namespace就包含了 Mapper 接口的全限定名称)得来的,含有一个 HashMap 类型的成员变量org.apache.ibatis.binding.MapperRegistry#knownMappers,key 是 Mapper 接口的Class对象,value 是org.apache.ibatis.binding.MapperProxyFactory,从名称就可以看出是用来创建 Mapper 接口的代理对象的工厂,后面会用到。
具体这个knownMappers是怎么填充的,详见org.apache.ibatis.binding.MapperRegistry#addMapper方法,暂时不管,先往下走:org.apache.ibatis.binding.MapperRegistry#getMapper
@SuppressWarnings(" unchecked" ) public < T> T getMapper(Class< T> type, SqlSession sqlSession) { final MapperProxyFactory< T> mapperProxyFactory = (MapperProxyFactory< T> ) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException(" Type " + type + " is not known to the MapperRegistry." ); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException(" Error getting mapper instance. Cause: " + e, e); } }

根据 Mapper 接口的类型,从knownMappers中拿到对应的工厂,然后创建代理对象,继续跟进:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) { final MapperProxy< T> mapperProxy = new MapperProxy< T> (sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }

这里又出现了一个MapperProxy对象,理解起来应该是一个代理对象,打开一看它实现了java.lang.reflect.InvocationHandler接口,这是挂羊头卖狗肉哇。
先不看狗肉了,继续跟进:org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.binding.MapperProxy< T> )
@SuppressWarnings(" unchecked" ) protected T newInstance(MapperProxy< T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }

在这,看到了熟悉的java.lang.reflect.Proxy,这里的mapperInterface是创建工厂时传入的 Mapper 接口。真正的 Mapper 接口的代理对象此时才产生,是真羊头。
这不是既熟悉又陌生的 JDK 动态代理么,说它熟悉,因为前面的狗肉mapperProxy是一个InvocationHandler对象,它拦截了所有对代理对象接口方法的调用。说它陌生是因为之前使用 JDK 动态代理时会有接口的具体实现子类,这里没看到。那就先看看org.apache.ibatis.binding.MapperProxy
/** * @author Clinton Begin * @author Eduardo Macarron */ public class MapperProxy< T> implements InvocationHandler, Serializable {private static final long serialVersionUID = -6424540398559729838L; private final SqlSession sqlSession; private final Class< T> mapperInterface; 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; }@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 判断 method 是不是 Object 类的方法,如 hashCode()、toString() if (Object.class.equals(method.getDeclaringClass())) { try {// 如果是,则调用当前 MapperProxy 对象的这些方法 // 跟 Mapper 接口的代理对象没关系 return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } // 到这了,说明调用的是接口中的方法,具体的执行就不是本次关注的重点了 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }// 对 MapperMethod 做了缓存,这个 methodCache 是个 ConcurrentHashMap,在 MapperProxyFactory 中创建的 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; }}

具体说明都在代码注释里面,没啥好说的了。
总结
  1. 通过 JDK 动态代理模式,创建 Mapper 接口的代理对象,拦截对接口方法的调用;
  2. Mapper 接口中不能使用重载,具体原因参见org.apache.ibatis.binding.MapperMethod.SqlCommand#SqlCommand,MyBatis 是通过mapperInterface.getName() + " ." + method.getName()去获取 xml 中解析出来的 SQL 的,具体可能还要看一下org.apache.ibatis.session.Configuration#mappedStatements
【MyBatis 中 Mapper 接口的使用原理】最后的最后,古话说的好:遇事不决,先开大龙(看源码)(逃

    推荐阅读