# 精尽 MyBatis 源码分析 —— Binding 模块 # 1. 概述 本文,我们来分享 MyBatis 的 Binding 模块,对应 `binding` 包。如下图所示:[`binding` 包](http://static.iocoder.cn/images/MyBatis/2020_02_07/01.png) 在 [《精尽 MyBatis 源码解析 —— 项目结构一览》](http://svip.iocoder.cn/MyBatis/intro) 中,简单介绍了这个模块如下: > 在调用 SqlSession 相应方法执行数据库操作时,需要指定映射文件中定义的 SQL 节点,如果出现拼写错误,我们只能在运行时才能发现相应的异常。为了尽早发现这种错误,MyBatis 通过 Binding 模块,将用户自定义的 Mapper 接口与映射配置文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。 > > 值得读者注意的是,开发人员无须编写自定义 Mapper 接口的实现,MyBatis 会自动为其创建动态代理对象。在有些场景中,自定义 Mapper 接口可以完全代替映射配置文件,但有的映射规则和 SQL 语句的定义还是写在映射配置文件中比较方便,例如动态 SQL 语句的定义。 本文涉及的类如下图所示:[类图](http://static.iocoder.cn/images/MyBatis/2020_02_07/02.png) # 2. MapperRegistry `org.apache.ibatis.binding.MapperRegistry` ,Mapper 注册表。 ## 2.1 构造方法 ``` // MapperRegistry.java /** * MyBatis Configuration 对象 */ private final Configuration config; /** * MapperProxyFactory 的映射 * * KEY:Mapper 接口 */ private final Map, MapperProxyFactory> knownMappers = new HashMap<>(); public MapperRegistry(Configuration config) { this.config = config; } ``` ## 2.2 addMappers `#addMappers(String packageName, ...)` 方法,扫描指定包,并将符合的类,添加到 `knownMappers` 中。代码如下: ``` // MapperRegistry.java public void addMappers(String packageName) { addMappers(packageName, Object.class); } public void addMappers(String packageName, Class superType) { // <1> 扫描指定包下的指定类 ResolverUtil> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set>> mapperSet = resolverUtil.getClasses(); // <2> 遍历,添加到 knownMappers 中 for (Class mapperClass : mapperSet) { addMapper(mapperClass); } } ``` - `<1>` 处,使用 ResolverUtil 扫描指定包下的指定类。详细解析,在 [《精尽 MyBatis 源码分析 —— IO 模块》](http://svip.iocoder.cn/MyBatis/io-package) 中有。 ## 2.3 hasMapper `#hasMapper(Class type)` 方法,判断是否有 Mapper 。代码如下: ``` // MapperRegistry.java public boolean hasMapper(Class type) { return knownMappers.containsKey(type); } ``` ## 2.4 addMapper `#addMapper(Class type)` 方法,添加到 `knownMappers` 中。代码如下: ``` // MapperRegistry.java public void addMapper(Class type) { // <1> 判断,必须是接口。 if (type.isInterface()) { // <2> 已经添加过,则抛出 BindingException 异常 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { // <3> 添加到 knownMappers 中 knownMappers.put(type, new MapperProxyFactory<>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. // <4> 解析 Mapper 的注解配置 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); // <5> 标记加载完成 loadCompleted = true; } finally { // <6> 若加载未完成,从 knownMappers 中移除 if (!loadCompleted) { knownMappers.remove(type); } } } } ``` - `<1>` 处,判断 `type` 必须是接口,也就是说 Mapper 接口。 - `<2>` 处,已经添加过,则抛出 BindingException 异常。 - `<3>` 处,添加到 `knownMappers` 中。 - `<4>` 处,解析 Mapper 的注解配置。 - `<5>` 处,创建 MapperAnnotationBuilder 对象,解析 Mapper 的注解配置。 - `<6>` 处,若加载未完成,从 `knownMappers` 中移除。 ## 2.5 getMapper `#getMapper(Class type, SqlSession sqlSession)` 方法,获得 Mapper Proxy 对象。代码如下: ``` // MapperRegistry.java public T getMapper(Class type, SqlSession sqlSession) { // <1> 获得 MapperProxyFactory 对象 final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type); // 不存在,则抛出 BindingException 异常 if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } // 创建 Mapper Proxy 对象 try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } ``` - `<1>` 处,从 `knownMappers` 中,获得 MapperProxyFactory 对象。 - `<2>` 处,调用 `MapperProxyFactory#newInstance(SqlSession sqlSession)` 方法,创建 Mapper Proxy 对象。详细解析,见 [「3. MapperProxyFactory」](https://svip.iocoder.cn/MyBatis/binding-package/#) 。 # 3. MapperProxyFactory `org.apache.ibatis.binding.MapperProxyFactory` ,Mapper Proxy 工厂类。 ## 3.1 构造方法 ``` // MapperProxyFactory.java /** * Mapper 接口 */ private final Class mapperInterface; /** * 方法与 MapperMethod 的映射 */ private final Map methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class mapperInterface) { this.mapperInterface = mapperInterface; } public Class getMapperInterface() { return mapperInterface; } public Map getMethodCache() { return methodCache; } ``` ## 3.2 newInstance `#newInstance(...)` 方法,创建 Mapper Proxy 对象。代码如下: ``` // MapperProxyFactory.java 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); } ``` - 依然是稳稳的基于 JDK Proxy 实现,而 InvocationHandler 参数是 MapperProxy 对象。关于 MapperProxy ,详细解析,见 [「4. MapperProxy」](https://svip.iocoder.cn/MyBatis/binding-package/#) 。 # 4. MapperProxy `org.apache.ibatis.binding.MapperProxy` ,实现 InvocationHandler、Serializable 接口,Mapper Proxy 。关键是 `java.lang.reflect.InvocationHandler` 接口,你懂的。 ## 4.1 构造方法 ``` // MapperProxy.java /** * SqlSession 对象 */ private final SqlSession sqlSession; /** * Mapper 接口 */ private final Class mapperInterface; /** * 方法与 MapperMethod 的映射 * * 从 {@link MapperProxyFactory#methodCache} 传递过来 */ private final Map methodCache; public MapperProxy(SqlSession sqlSession, Class mapperInterface, Map methodCache) { this.sqlSession = sqlSession; this.mapperInterface = mapperInterface; this.methodCache = methodCache; } ``` ## 4.2 invoke `#invoke(Object proxy, Method method, Object[] args)` 方法,调用方法。代码如下: ``` // MapperProxy.java @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // <1> 如果是 Object 定义的方法,直接调用 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); // 见 https://github.com/mybatis/mybatis-3/issues/709 ,支持 JDK8 default 方法 } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // <3.1> 获得 MapperMethod 对象 final MapperMethod mapperMethod = cachedMapperMethod(method); // <3.2> 执行 MapperMethod 方法 return mapperMethod.execute(sqlSession, args); } ``` - `<1>` 处,如果是 Object 定义的方法,直接调用。 - `<2>` 处,调用 `#isDefaultMethod((Method method)` 方法,判断是否为 `default` 修饰的方法,若是,则调用 `#invokeDefaultMethod(Object proxy, Method method, Object[] args)` 方法,进行反射调用。代码如下: ``` // MapperProxy.java private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { final Constructor constructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); if (!constructor.isAccessible()) { constructor.setAccessible(true); } final Class declaringClass = method.getDeclaringClass(); return constructor .newInstance(declaringClass, MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC) .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args); } /** * Backport of java.lang.reflect.Method#isDefault() */ private boolean isDefaultMethod(Method method) { return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC && method.getDeclaringClass().isInterface(); } ``` - JDK8 在接口上,新增了 `default` 修饰符。怎么进行反射调用,见 [《java8 通过反射执行接口的default方法》](https://www.jianshu.com/p/63691220f81f) 一文。 - 关于这段代码,在 https://github.com/mybatis/mybatis-3/issues/709 上有讨论。 - `<3.1>` 处,调用 `#cachedMapperMethod(Method method)` 方法,获得 MapperMethod 对象。代码如下: ``` // MapperProxy.java private MapperMethod cachedMapperMethod(Method method) { return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } ``` - 默认从 `methodCache` 缓存中获取。如果不存在,则进行创建,并进行缓存。 - `<3.2>` 处,调用 `MapperMethod#execute(SqlSession sqlSession, Object[] args)` 方法,执行 MapperMethod 方法。关于 MapperMethod ,在 [「5. MapperMethod」](https://svip.iocoder.cn/MyBatis/binding-package/#) 中,详细解析。 # 5. MapperMethod `org.apache.ibatis.binding.MapperMethod` ,Mapper 方法。在 Mapper 接口中,每个定义的方法,对应一个 MapperMethod 对象。 ## 5.1 构造方法 ``` // MapperMethod.java /** * SqlCommand 对象 */ private final SqlCommand command; /** * MethodSignature 对象 */ 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); } ``` - `command` 属性,SqlCommand 对象。关于它的详细解析,见 [「6. SqlCommand」](https://svip.iocoder.cn/MyBatis/binding-package/#) 。 - `method` 属性,MethodSignature 对象。关于它的详细解析,见 [「7. MethodSignature」](https://svip.iocoder.cn/MyBatis/binding-package/#) 。 ## 5.2 execute 因为涉及比较多的后面的内容,所以放在 详细解析。 心急的胖友,可以先看看 [《Mybatis3.3.x技术内幕(十一):执行一个Sql命令的完整流程》](https://my.oschina.net/zudajun/blog/670373) 。 # 6. SqlCommand SqlCommand ,是 MapperMethod 的内部静态类,SQL 命令。 ## 6.1 构造方法 ``` // SqlCommand.java /** * {@link MappedStatement#getId()} */ private final String name; /** * SQL 命令类型 */ private final SqlCommandType type; public SqlCommand(Configuration configuration, Class mapperInterface, Method method) { final String methodName = method.getName(); final Class declaringClass = method.getDeclaringClass(); // <1> 获得 MappedStatement 对象 MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration); // <2> 找不到 MappedStatement if (ms == null) { // 如果有 @Flush 注解,则标记为 FLUSH 类型 if (method.getAnnotation(Flush.class) != null) { name = null; type = SqlCommandType.FLUSH; } else { // 抛出 BindingException 异常,如果找不到 MappedStatement throw new BindingException("Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName); } // <3> 找到 MappedStatement } else { // 获得 name name = ms.getId(); // 获得 type type = ms.getSqlCommandType(); if (type == SqlCommandType.UNKNOWN) { // 抛出 BindingException 异常,如果是 UNKNOWN 类型 throw new BindingException("Unknown execution method for: " + name); } } } ``` - `name` 属性,对应 `MappedStatement#getId()` 方法获得的标识。实际上,就是 `${NAMESPACE_NAME}.${语句_ID}`, 例如:`"org.apache.ibatis.autoconstructor.AutoConstructorMapper.getSubject2"` 。 - `type` 属性,SQL 命令类型。`org.apache.ibatis.mapping.SqlCommandType` 类,代码如下: ``` // SqlCommandType.java public enum SqlCommandType { /** * 未知 */ UNKNOWN, /** * 插入 */ INSERT, /** * 更新 */ UPDATE, /** * 删除 */ DELETE, /** * 查询 */ SELECT, /** * FLUSH */ FLUSH } ``` - `<1>` 处,调用 `#resolveMappedStatement(Class mapperInterface, String methodName, Class declaringClass, Configuration configuration)` 方法,获得 MappedStatement 对象。详细解析,胖友先跳到 [「6.2 resolveMappedStatement」](https://svip.iocoder.cn/MyBatis/binding-package/#) 。 - `<2>` 处,如果找不到 MappedStatement 对象,说明该方法上,没有对应的 SQL 声明。那么在判断是否有 `@Flush` 注解,如果有,说明该方法是用于执行 flush 操作,否则,抛出 BindingException 异常。 - `<3>` 处,如果找到 MappedStatement 对象,则初始化 `name` 和 `type` 属性。 ## 6.2 resolveMappedStatement `#resolveMappedStatement(Class mapperInterface, String methodName, Class declaringClass, Configuration configuration)` 方法,获得 MappedStatement 对象。代码如下: ``` // SqlCommand.java private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName, Class declaringClass, Configuration configuration) { // <1> 获得编号 String statementId = mapperInterface.getName() + "." + methodName; // <2> 如果有,获得 MappedStatement 对象,并返回 if (configuration.hasStatement(statementId)) { return configuration.getMappedStatement(statementId); // 如果没有,并且当前方法就是 declaringClass 声明的,则说明真的找不到 } else if (mapperInterface.equals(declaringClass)) { return null; } // 遍历父接口,继续获得 MappedStatement 对象 for (Class superInterface : mapperInterface.getInterfaces()) { if (declaringClass.isAssignableFrom(superInterface)) { MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration); if (ms != null) { return ms; } } } // 真的找不到,返回 null return null; } ``` - `<1>` 处,获得编号。这个编号,和我们上文提到的 `${NAMESPACE_NAME}.${语句_ID}` 。 - `<2>` 处,通过 `Configuration#hasStatement(String statementId)` 方法,判断是否有 MappedStatement 。如果有,则调用 `Configuration#getMappedStatement(String statementId)` 方法,获得 MappedStatement 对象。关于 Configuration ,我们在后续的文章中,详细解析。在这里,胖友只需要知道,Configuration 里缓存了所有的 MappedStatement ,并且每一个 XML 里声明的例如 `