场景复现
最近在开发一个需求的时候,因为公司使用的Mybatis作为持久层框架,然后有一个需求是通过用户UID或者身份证号进行查询,于是在mapper中写下了如下语句(因为公司内容敏感,所以自己建的新表进行模拟,含义一致):
/** * 查询用户信息 * * @param uid 用户UID * @param userStatus 用户状态 * @return */ @Select("select uid,card_name,card_no,user_status from t_user t where t.uid = #{uid} and t.user_status = #{userStatus}") User selectByCondition(@Param("uid") Long uid,@Param("userStatus") String userStatus); /** * 查询用户信息 * * @param cardNo 身份证号 * @param userStatus 用户状态 * @return */ @Select("select uid,card_name,card_no,user_status from t_user t where t.card_no = #{cardNo} and t.user_status = #{userStatus}") User selectByCondition(@Param("cardNo") String cardNo,@Param("userStatus") String userStatus);
PS:通过UID查询以下简称“方法一”,通过身份证号查询以下简称“方法二” 。
因为查询需要关联用户状态一起查询,自然而然想到使用方法重载,应用启动时并没有任何错误,
在执行方法一查询时也正常执行,但是在执行方法二查询时,也就是调用重载方法时却报了如下错误:
Caused by: org.apache.ibatis.binding.BindingException: Parameter 'uid' not found. Available parameters are [userStatus, cardNo, param1, param2]
at org.apache.ibatis.binding.MapperMethod$ParamMap.get(MapperMethod.java:212)
at org.apache.ibatis.reflection.wrapper.MapWrapper.get(MapWrapper.java:45)
at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:122)
at org.apache.ibatis.executor.BaseExecutor.createCacheKey(BaseExecutor.java:219)
at org.apache.ibatis.executor.CachingExecutor.createCacheKey(CachingExecutor.java:146)
at com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor.intercept(MybatisPlusInterceptor.java:80)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:62)
at com.sun.proxy.$Proxy90.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427)
... 37 more
错误真的报的不知所措,方法二这个查询语句根本就没有UID参数,为什么执行报UID找不到?
观察了老半天,最后在 ctrl+f 的时候发现有两个方法被高亮了,那是不是因为方法重载导致的,因为方法一是有uid这个参数的,然后想重命名方法二,神奇的事情发生了,方法一的名称也跟着变了,这下更加确认了,Mybatis是不是把重载的方法视为同一个方法了?
带着疑问,唯有源码解惑也。我们的目标就是需要找到Mybatis是如何解析mapper并生成代理对象的。
原理分析
因为我们是采用@MapperScan(value = "com.github.repository.mapper")注解进行mapper扫描的,那么我们进入该注解:
package org.mybatis.spring.annotation;
// 导包忽略,有兴趣的同学自行查看
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({MapperScannerRegistrar.class})
@Repeatable(MapperScans.class)
public @interface MapperScan {
// 属性忽略,有兴趣的同学自行查看
}
其他的属性咱们暂时不关注,我们关注到该注解Import了一个对象MapperScannerRegistrar,名字非常明显,就是mapper扫描注册,我们知道@Import注解会将目标对象注册为Spring Bean对象,所以我们进入到对象:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.mybatis.spring.annotation;
// 导包暂时忽略,有兴趣的同学自行查看
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
this.registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0));
}
}
// 其余方法暂时忽略,有兴趣的同学自行查看
}
我们发现该对象实现了Spring的接口ImportBeanDefinitionRegistrar,而当MapperScannerRegistrar使用@Import的方式注入IOC时,其重写的方法registerBeanDefinitions会被Spring自动回调,我们把聚光灯聚焦到这个方法:
这个方法在读取了MapperScan所有属性,于是我们进入到重载的内部方法:
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
// …… 省略中间细节代码,我们只关注注入的对象
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
我们会发现其将MapperScannerConfigurer进行了注册,那么进入到这个类中:
package org.mybatis.spring.mapper;
// 省略导入包
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
// 暂时省略具体方法
}
我们发现该对象实现了BeanDefinitionRegistryPostProcessor,所以Spring会自动回调其重写的方法postProcessBeanDefinitionRegistry,所以我们进入到该方法:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
// 注意这个对象
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
// ……
// 省略中间部分内容,有兴趣的同学可以自行查看
// 关注这个方法
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
这里我们看到扫描使用的对象是ClassPathMapperScanner,我们进入其scan方法:
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
根据命名,我们猜测实际扫描mapper的方法应该在 doScan中,于是我们进入ClassPathMapperScanner的doScan方法:
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
这里首先就在获取bean定义持有者,我们看下走到这步,里面都有啥:
我们发现获取到的就是根据我们MapperScan注解配置的basePackages所扫描到的mapper,那既然mapper对象获取到了,那接下来很明显就是processBeanDefinitions进行mapper解析了,于是我们进入到该方法:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
// ……
// 省略部分代码
// 我们主要关注这行代码
definition.setBeanClass(this.mapperFactoryBeanClass);
// ……
// 省略部分代码
}
}
奇怪了,明明是要解析mapper生成代理对象并注册IOC,这怎么把beanClass设置成另外一个对象了?这里我们先看下mapperFactoryBeanClass是什么,我们点击跳转,发现其值为:
private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;
哦,是一个mapper工厂bean,这里我们简单提一下工厂bean和bean工厂的区别:
- bean工厂实现的接口是BeanFactory,工厂bean实现的接口是FactoryBean。
- bean工厂不是bean对象,它是负责生产和维护IOC中的所有bean对象,工厂bean是一种特殊bean对象,用来生产和加工新的bean对象,并通过getObject获取新的对象。
也就是说我们的mapper对象会通过 MapperFactoryBean生产并加工出来,那我们接下来看下这个工厂bean是如何加工mapper对象的:
package org.mybatis.spring.mapper;
// 暂时省略导包
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
// 暂时省略内部实现
}
我们来看下MapperFactoryBean的类图:
我们发现其继承了DaoSupport,我们都知道一个类如果继承了DaoSupport这个类,在注入IOC时
其重写的checkDaoConfig的就会被Spring自动回调,那我们来看checkDaoConfig做了什么事情:
@Override
protected void checkDaoConfig() {
super.checkDaoConfig();
notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
// 重点看这个方法
configuration.addMapper(this.mapperInterface);
} catch (Exception e) {
logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);
throw new IllegalArgumentException(e);
} finally {
ErrorContext.instance().reset();
}
}
}
很明显,其他方法都是辅助,只有configuration.addMapper(this.mapperInterface);需要我们关心,于是我们把断点打到这行:
我们会发现正是我们的mapper对象,于是我们点入这个方法,我们一路断点进去,我们会发现一句这样的代码:
答案最终揭晓 ,重点来了,同学们,构建mapperId时是通过类名加方法名构建,我们一旦使用重载,势必导致ID冲突,因为ID的构建并没有将形参类型,形参个数,形参顺序计算进去,这就解释了为什么我们在使用方法二时却引用了方法一。
到这里,各位同学应该已经了解了Mybatis为啥不能兼容方法重载了,至于Mybatis为什么这么设计,为什么构建mapperId时不把方法重载考虑进去,这个我们后续分析Mybatis源码的时候再继续分析其更深层次原因。
解决办法
- 不使用方法重载
既然从源码的角度,Mybatis不支持方法重载,那使用时就尽量避开,例如我们方法一就可以命名为selectByUidAndUserStatus,方法二就可以命名为selectByCardNoAndUserStatus,这样就避开了方法重载,避免了在报错时找不到问题所在。
- 使用动态SQL
那既然不支持重载,那我们就写一个SQL呗,利用Mybatis原生提供的动态SQL进行构建,例如我们可以将方法一和方法二改造如下:
@Select(" <script> " +
" select uid,card_name,card_no,user_status from t_user t " +
" <where>" +
" <if test = 'uid != null'>" +
" and t.uid = #{uid}" +
" </if>" +
" <if test = 'cardNo != null and cardNo.length > 0'>" +
" and t.card_no = #{cardNo}" +
" </if>" +
" and t.user_status = #{userStatus}" +
" </where>" +
" </script> ")
User selectByCondition(@Param("uid") Long uid, @Param("cardNo") String cardNo, @Param("userStatus") String userStatus);
- 使用default关键字(不推荐)
JDK1.8及以上,提供了default关键字,允许在接口里面编写default方法并给予实现,这种方法虽然可以用,但是不推荐,因为mapper中就是定义的接口,写了这个虽然不影响使用,但是有点违背Mybatis的设计理念,并且mapper接口中最好保持简单纯净。
以上就是关于Mybatis的mapper接口中无法使用方法重载的原因分析,如果各位同学觉得对你有所帮助,请关注、点赞、评论、收藏来支持我,手头宽裕的话也可以赞赏来表达各位的认可,各位同学的支持是对我最大的鼓励。未来为大家带来更好的创作。
分享一句非常喜欢的话:把根牢牢扎深,再等春风一来,便会春暖花开。
版权声明:以上引用信息以及图片均来自网络公开信息,如有侵权,请留言或联系
504401503@qq.com,立马删除。