Mybatis和方法重载只能二选一?

场景复现

最近在开发一个需求的时候,因为公司使用的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工厂的区别:

  1. bean工厂实现的接口是BeanFactory,工厂bean实现的接口是FactoryBean。
  2. 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,立马删除。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖啡攻城狮Alex

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值