对于两个拥有相同方法名,但入参不同的方法,Mybatis会如何处理?
@Select("sql xxx") Long statisticTotal(@Param("beginTime") String beginTime, @Param("endTime") String endTime); @Select("sql xxx") Long statisticTotal(@Param("name") String name);
从一些地方了解到,Mybatis 其实就是在运行时,对于每个 Mapper 的方法调用最终都会被一个代理所捕获:
// 在Mybatis中是在org.apache.ibatis.binding.MapperProxy // MyBatisPlus中是在com.baomidou.mybatisplus.core.override.MybatisMapperProxy @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 { return cachedInvoker(method).invoke(proxy, method, args, sqlSession); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } }
在 cachedInvoker 方法中,其会将接口的方法进行包装为 DefaultMethodInvoker 或者 PlainMethodInvoker:
return methodCache.computeIfAbsent(method, m -> { if (m.isDefault()) { try { if (privateLookupInMethod == null) { return new DefaultMethodInvoker(getMethodHandleJava8(method)); } else { return new DefaultMethodInvoker(getMethodHandleJava9(method)); } } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException(e); } } else { return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); } });
其中 DefaultMethodInvoker 是为了实现默认方法的调用而实现的,重点还是 PlainMethodInvoker,其创建时又需要传递一个 MapperMethod,这个 MapperMethod 就是在 cachedInvoker 后 invoke 的东西,在 MyBatisPlus 中这个类是 MybatisMapperMethod。
在 MapperMethod 创建时,会将接口的方法转为一条命令:
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
其内部就是根据 Mapper 接口名称以及方法在配置中寻找对应的 Statement,并且在 resolveMappedStatement 方法中,它生成了一个 statementId 就是用来寻找 Statement:
// MapperMethod String statementId = mapperInterface.getName() + "." + methodName;
由此可见,参数并不构成这个唯一ID,至此 Command 的内容就差不多清楚了 而且这个 statementId 就是command 的name:
// MapperMethod.SqlCommand name = ms.getId(); type = ms.getSqlCommandType();
回到 MapperMethod 的 execute(MyBatisPlus中是invoke) 代理捕获调用后来到这里 根据先前的 command 的name 去调用sqlSession
而 sqlSession 则根据这个 name 去查找 MappedStatement,再根据 MappedStatement 以及传递过来的参数构造SQL语句,最后发到数据库去查询
// org.apache.ibatis.session.defaults.DefaultSqlSession MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); // org.apache.ibatis.executor.BaseExecutor BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
那么最后还有一个问题,就是两个同名的方法存在时,我去调用其中一个,却时好时坏,我的猜测是这个 MappedStatement 的加载顺序是不固定的,相同 id 的 Statement 会导致后面扫描到的覆盖之前的,但如果这样的话,结果应该是确定的,为什么有时报错有时却不报错?
但是在刚才的调用链路中似乎并没有发现是如何加载 Statement 的,于是换个思路,从 SpringBoot 的自动装配来着手,在 MybatisPlus starter 的 jar 包下,有个 spring.factories 的文件 里面记录了要自动装配哪些类。
当然就发现了 MybatisPlusAutoConfiguration,里面有个内部类:AutoConfiguredMapperScannerRegistrar 看名字就是这个了
这个类声明一大堆对Mapper的描述为BeanDefinition 并交由 MapperScannerConfigurer 来进行扫描,经过一顿扫描,但在这里没怎么读懂把 Configuration 注入到 Spring 的,我猜大概是利用了 Spring 的一些机制,最后是会调用到:
configuration.addMapper(this.mapperInterface);
最后来到MapperRegistry (MyBatisMapperRegistry),这里会对每个Mapper接口的方法进行处理:
// addMapper MybatisMapperAnnotationBuilder parser = new MybatisMapperAnnotationBuilder(config, type); parser.parse(); // 对Mapper接口的每个方法做处理 for (Method method : type.getMethods()) { if (!canHaveStatement(method)) { continue; } if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null) { parseResultMap(method); } try { // TODO 加入 注解过滤缓存 InterceptorIgnoreHelper.initSqlParserInfoCache(cache, mapperName, method); parseStatement(method); } catch (IncompleteElementException e) { // TODO 使用 MybatisMethodResolver 而不是 MethodResolver configuration.addIncompleteMethod(new MybatisMethodResolver(this, method)); } }
重点就在于 type.getMethods 这个地方,随着我每次启动JVM,每次获取的 methods 返回的数组顺序是不一样的,这就导致我之前那个时好时坏的问题,后面的相同的 statementId 覆盖了之前的,但是谁先谁后并不是一个确定性的行为,Class.getMethods 的文档其实也说明了:
// The elements in the returned array are not sorted and are not in any particular order // 返回的元素没有特定顺序
调试直到这里,也是印证了我的猜测,不过问题不出在MyBatis,而是出在JDK的反射机制上