mybatis升级到mybatis plus后报Parameter not found

前端时间把老系统的代码迁移到mybatis-plus中,发现将原有的RowBounds分页参数替换为IPage<>后,原有的查询无法正常使用,报错

sql 复制代码
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]

下边把分析和解决的流程记录下。

1.分析问题

首先第一反应因为是不同的mybatis版本造成的,对比maven后发现没有问题。那么就一步步debug查询入馆了。我们知道mybatis的主要的逻辑都是依靠JDK动态代理实现的。在加载的过程中,把接口类,每一个都在spring中注入为MapperFactoryBean这个代理类,正好来看一下mybatis-plus是怎么加强mybatis的 先搜一下mp的依赖,找个有眼缘的

第一步找spring.factories文件,这个是自动加载的配置文件,里面配置了很多自动加载的类,

凭借多年的经验,直接找到MybatisPlusAutoConfiguration类 这个里边我们有两个需要关注的方法

java 复制代码
@Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // 使用 MybatisSqlSessionFactoryBean 替代 SqlSessionFactoryBean
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }
        // 使用了自己的configuration配置类,MybatisConfiguration是实现plus相关功能关键
        applyConfiguration(factory);
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }
        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }
        if (this.properties.getTypeAliasesSuperType() != null) {
            factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
        }
        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }
        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }
        // x.xml的扫描位置,
        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }
        // TODO 修改源码支持定义 TransactionFactory
        this.getBeanThen(TransactionFactory.class, factory::setTransactionFactory);

![img_1.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/768c07c735a04776ba9a8e9a23363d8d~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5bCP5bCPTElO5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1758879435&x-signature=1FLxMEunEzWIm0th%2BRK%2Bo8rjiQQ%3D)
![img.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/269ce4b324e443ee9134496bb5c02613~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5bCP5bCP5bCPTElO5a2Q:q75.awebp?rk3s=f64ab15b&x-expires=1758879435&x-signature=eqxlGAgBYCBwzQEZTPO%2F5HeKwnc%3D)
        // TODO 对源码做了一定的修改(因为源码适配了老旧的mybatis版本,但我们不需要适配)
        Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }
        Optional.ofNullable(defaultLanguageDriver).ifPresent(factory::setDefaultScriptingLanguageDriver);

        applySqlSessionFactoryBeanCustomizers(factory);

        // TODO 此处必为非 NULL
        GlobalConfig globalConfig = this.properties.getGlobalConfig();
        // TODO 注入填充器 就是自动更新时间和用户字段的东西了
        this.getBeanThen(MetaObjectHandler.class, globalConfig::setMetaObjectHandler);
        // TODO 注入注解控制器
        this.getBeanThen(AnnotationHandler.class, globalConfig::setAnnotationHandler);
        // TODO 注入参与器
        this.getBeanThen(PostInitTableInfoHandler.class, globalConfig::setPostInitTableInfoHandler);
        // TODO 注入主键生成器
        this.getBeansThen(IKeyGenerator.class, i -> globalConfig.getDbConfig().setKeyGenerators(i));
        // TODO 注入sql注入器 这个就是MP自动填充insert这些基本方法的类了
        this.getBeanThen(ISqlInjector.class, globalConfig::setSqlInjector);
        // TODO 注入ID生成器
        this.getBeanThen(IdentifierGenerator.class, globalConfig::setIdentifierGenerator);
        // TODO 设置 GlobalConfig 到 MybatisSqlSessionFactoryBean
        factory.setGlobalConfig(globalConfig);
        return factory.getObject();
    }
    

    // TODO 入参使用 MybatisSqlSessionFactoryBean
    private void applyConfiguration(MybatisSqlSessionFactoryBean factory) {
        // TODO 使用 MybatisConfiguration
        MybatisPlusProperties.CoreConfiguration coreConfiguration = this.properties.getConfiguration();
        MybatisConfiguration configuration = null;
        if (coreConfiguration != null || !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new MybatisConfiguration();
        }
        if (configuration != null && coreConfiguration != null) {
            coreConfiguration.applyTo(configuration);
        }
        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
            for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
                customizer.customize(configuration);
            }
        }
        factory.setConfiguration(configuration);
    }

在这个类里我们看到有一个类

java 复制代码
public static class AutoConfiguredMapperScannerRegistrar
        implements BeanFactoryAware, EnvironmentAware, ImportBeanDefinitionRegistrar {
    // ......
        }

条件反射我还以为这个是替代mybatis扫码接口的,结果并不是,打了断点也没有,这边重点的就是MybatisSqlSessionFactoryBeanMybatisConfiguration这两个类,这个更换后就会把Mapper的动态代理的创建都走了新的逻辑。 在这里我们只能看到resolveMapperLocations 这个是扫描那些xml文件的, 其实Mapper接口类的扫描是另外一个类MapperScannerRegistrar, 这个类在在注解@MapperScan中被引入

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {}

分别看看基于xml文件的mapper和基于注解的mapper是怎么处理的

1.基于xml的mapper

java 复制代码
public class MybatisSqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void afterPropertiesSet() throws Exception {
        notNull(dataSource, "Property 'dataSource' is required");
        state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
            "Property 'configuration' and 'configLocation' can not specified with together");
        this.sqlSessionFactory = buildSqlSessionFactory();
    }


    /**
     * Build a {@code SqlSessionFactory} instance.
     * <p>
     * The default implementation uses the standard MyBatis {@code XMLConfigBuilder} API to build a
     * {@code SqlSessionFactory} instance based on an Reader. Since 1.3.0, it can be specified a
     * {@link Configuration} instance directly(without config file).
     * </p>
     *
     * @return SqlSessionFactory
     * @throws IOException if loading the config file failed
     */
    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

        final Configuration targetConfiguration;
        if (this.mapperLocations != null) {
            if (this.mapperLocations.length == 0) {
                LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
            } else {
                for (Resource mapperLocation : this.mapperLocations) {
                    if (mapperLocation == null) {
                        continue;
                    }
                    try {
                        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                            targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
                        xmlMapperBuilder.parse();
                    } catch (Exception e) {
                        throw new IOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
                    } finally {
                        ErrorContext.instance().reset();
                    }
                    LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
                }
            }
        } else {
            LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
        }
    }
}

buildSqlSessionFactory方法中会加载xml文件,生成XMLMapperBuilder对象,这个类是mybatis的类

java 复制代码
public class XMLMapperBuilder extends BaseBuilder {
    // ......
    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
            configurationElement(parser.evalNode("/mapper"));
            configuration.addLoadedResource(resource);
            bindMapperForNamespace();
        }

        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }

    private void bindMapperForNamespace() {
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
            Class<?> boundType = null;
            try {
                boundType = Resources.classForName(namespace);
            } catch (ClassNotFoundException e) {
                // ignore, bound type is not required
            }
            if (boundType != null && !configuration.hasMapper(boundType)) {
                // Spring may not know the real resource name so we set a flag
                // to prevent loading again this resource from the mapper interface
                // look at MapperAnnotationBuilder#loadXmlResource
                configuration.addLoadedResource("namespace:" + namespace);
                configuration.addMapper(boundType);
            }
        }
    }
}

namespace就是xml文件中<mapper namespace="com.example.XXXMapper">的值,这个值必须是一个接口类,mybatis会把这个接口类动态代理生成一个实现类,然后注入到spring中。 configuration.addMapper(boundType);这个方法就是把接口类注册到configuration中,这里边的configurationMybatisConfiguration

2.基于注解的mapper

基于注解的mapper是通过MapperScannerRegistrar类注入的MapperScannerConfigurer类进行扫描注入的,而MapperScannerConfigurer 又是实现了BeanDefinitionRegistryPostProcessor接口,所以可以在spring启动过程中 加入bean定义,最后又是通过ClassPathMapperScanner 这个类进行具体的扫描工作,

java 复制代码
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {

    private Class<? extends MapperFactoryBean> mapperFactoryBeanClass = MapperFactoryBean.class;

    private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
        AbstractBeanDefinition definition;
        BeanDefinitionRegistry registry = getRegistry();
        for (BeanDefinitionHolder holder : beanDefinitions) {
            definition = (AbstractBeanDefinition) holder.getBeanDefinition();
            boolean scopedProxy = false;
            String beanClassName = definition.getBeanClassName();
            LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
                + "' mapperInterface");

            // the mapper interface is the original class of the bean
            // but, the actual class of the bean is MapperFactoryBean
            definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
            try {
                // for spring-native
                definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName));
            } catch (ClassNotFoundException ignore) {
                // ignore
            }

            definition.setBeanClass(this.mapperFactoryBeanClass);

            // ...
        }
    }
}

可以看到这里通过MapperFactoryBean 这个类把接口类动态代理生成一个实现类,然后注入到spring中。

java 复制代码
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
    @Override
    public T getObject() throws Exception {
        return getSqlSession().getMapper(this.mapperInterface);
    }

    @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();
            }
        }
    }
}

可以看到MapperFactoryBean继承自SqlSessionDaoSupport类,这个类主要是用来获取SqlSession对象的,而SqlSession对象是mybatis的核心对象,所有的增删改查都是通过这个对象来实现的。 当然这边我们可以看到sqlSessionFactory是通过SqlSessionDaoSupport类注入的,而SqlSessionFactory是通过MybatisSqlSessionFactoryBean类生成的。DaoSupport 实现了InitializingBean接口,所以在spring容器初始化的时候会调用afterPropertiesSet方法,这个方法会调用checkDaoConfig方法, MapperFactoryBean中重写了这个方法,主要是把接口类注册到configuration中。实际上是调用MybatisMapperRegistry类的addMapper方法注册的。最后调用的MybatisMapperAnnotationBuilderparse方法解析注解生成MappedStatement对象。 里边就有对默认的insert等方法的sql语句的生成。

java 复制代码
public  abstract class SqlSessionDaoSupport extends DaoSupport {

  private SqlSessionTemplate sqlSessionTemplate;
  
  public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
    if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {
      this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);
    }
  }
}


public abstract class DaoSupport implements InitializingBean {
    protected final Log logger = LogFactory.getLog(this.getClass());

    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        this.checkDaoConfig();

        try {
            this.initDao();
        } catch (Exception ex) {
            throw new BeanInitializationException("Initialization of DAO failed", ex);
        }
    }

    protected abstract void checkDaoConfig() throws IllegalArgumentException;

    protected void initDao() throws Exception {
    }
}

public class SqlSessionTemplate implements SqlSession, DisposableBean {
    @Override
    public <T> T getMapper(Class<T> type) {
        return getConfiguration().getMapper(type, this);

    }
}

到这里我们可以看到不管是基于xml的mapper还是基于注解的mapper,最终都是通过configuration.getMapper(Class<T> type)方法来获取mapper接口的实现类的,而这个实现类是mybatis动态代理生成的。 当然这边的configurationMybatisConfiguration类,这个类是mybatis-plus对mybatis的增强类。

java 复制代码
public class MybatisConfiguration extends Configuration {
    protected final MybatisMapperRegistry mybatisMapperRegistry = new MybatisMapperRegistry(this);
    /**
     * 使用自己的 MybatisMapperRegistry
     */
    @Override
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        return mybatisMapperRegistry.getMapper(type, sqlSession);
    }

}

这里我们发现MybatisConfiguration类使用了自己的MybatisMapperRegistry类来注册和获取mapper接口的实现类的,而不是使用mybatis默认的MapperRegistry类。

java 复制代码
public class MybatisMapperRegistry extends MapperRegistry {

    @Override
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // TODO 这里换成 MybatisMapperProxyFactory 而不是 MapperProxyFactory
        // fix https://github.com/baomidou/mybatis-plus/issues/4247
        MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
                .filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
                .orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
        }
        try {
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }
}

好了马上就到了JDK动态代理生成实现类的关键代码了,这里我们发现MybatisMapperRegistry类使用了自己的MybatisMapperProxyFactory类来生成mapper接口的实现类的,而不是使用mybatis默认的MapperProxyFactory类。

java 复制代码
public class MybatisMapperProxyFactory<T> {

    @Getter
    private final Class<T> mapperInterface;
    @Getter
    private final Map<Method, MybatisMapperProxy.MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

    public MybatisMapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MybatisMapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        final MybatisMapperProxy<T> mapperProxy = new MybatisMapperProxy<>(sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }
}

这里的MapperProxy也是自己的MybatisMapperProxy类,而不是mybatis默认的MapperProxy类。

java 复制代码
public class MybatisMapperProxy<T> implements InvocationHandler, Serializable {
    private static class PlainMethodInvoker implements MapperMethodInvoker {
        private final MybatisMapperMethod mapperMethod;

        public PlainMethodInvoker(MybatisMapperMethod mapperMethod) {
            super();
            this.mapperMethod = mapperMethod;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
            return mapperMethod.execute(sqlSession, args);
        }
    }
}
java 复制代码
public class MybatisMapperMethod {
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        switch (command.getType()) {

            case SELECT:
                // TODO 这里下面改了
                if (IPage.class.isAssignableFrom(method.getReturnType())) {
                    result = executeForIPage(sqlSession, args);
                    // TODO 这里上面改了
                } else {
                    Object param = method.convertArgsToSqlCommandParam(args);
                    result = sqlSession.selectOne(command.getName(), param);
                    if (method.returnsOptional()
                        && (result == null || !method.getReturnType().equals(result.getClass()))) {
                        result = Optional.ofNullable(result);
                    }
                }
                break;
            return result;
        }
    }

    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;
        Object param = method.convertArgsToSqlCommandParam(args);
        if (method.hasRowBounds()) {
            RowBounds rowBounds = method.extractRowBounds(args);
            result = sqlSession.selectList(command.getName(), param, rowBounds);
        } else {
            result = sqlSession.selectList(command.getName(), param);
        }
        // issue #510 Collections & arrays support
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
            if (method.getReturnType().isArray()) {
                return convertToArray(result);
            } else {
                return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
            }
        }
        return result;
    }
    

}

这边也可以看到返回IPage类型的查询会走executeForIPage方法,这个方法是mybatis-plus新增的分页查询方法。其实就是调用了selectList方法查询数,最后把结果封装到IPage对象的records中返回。 随便找一个查询逻辑,我们发现参数是在method.convertArgsToSqlCommandParam(args)中进行处理的,这个调用了mybatis原有的逻辑,最终调用的是ParamNameResolver类,这个类是mybatis用来处理参数的类。 最终我们含有IPage的参数被封装成了arg0, arg1这种形式的参数,导致在xml文件中找不到对应的参数,所以报错Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]

java 复制代码
public class MapperMethod {
public static class MethodSignature {

    private final ParamNameResolver paramNameResolver;

    public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
      // ,...
        // 
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }
  }

}

public class ParamNameResolver {

    public static final String GENERIC_NAME_PREFIX = "param";

    private final boolean useActualParamName;
    private final SortedMap<Integer, String> names;

    private boolean hasParamAnnotation;

    public ParamNameResolver(Configuration config, Method method) {
        this.useActualParamName = config.isUseActualParamName();
        final Class<?>[] paramTypes = method.getParameterTypes();
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        final SortedMap<Integer, String> map = new TreeMap<>();
        int paramCount = paramAnnotations.length;
        // get names from @Param annotations
        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
            // 重点是这个isSpecialParameter
            if (isSpecialParameter(paramTypes[paramIndex])) {
                // skip special parameters
                continue;
            }
           // 处理@Param注解 隐藏了
            map.put(paramIndex, name);
        }
        names = Collections.unmodifiableSortedMap(map);
    }
}

ParamNameResolver这个类重点看下构造方法开始的时候,isSpecialParameter方法就是关键所在,

java 复制代码
    private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz);
}

其实到这里大家就能知道问题所在了,RowBounds是mybatis的分页参数,而IPage是mybatis-plus的分页参数,这里判断参数类型的时候没有把IPage考虑进去,所以会报错找不到参数。

2.解决问题

根据刚才的问题所在我们可以想象解决问题也很简单,我们只需要把isSpecialParameter方法改成下面这样

java 复制代码
    private static boolean isSpecialParameter(Class<?> clazz) {
    return RowBounds.class.isAssignableFrom(clazz) || ResultHandler.class.isAssignableFrom(clazz)
        || IPage.class.isAssignableFrom(clazz);
}

这样就能把IPage参数也当做特殊参数处理了,就不会报错了。 但是这是有个问题的,修改源码重新打包不现实,如果像mybatis-plus这样把所有的类都实现一遍也更费事,而且在mybatis中他会把rowBounds参数单独处理,但是IPage对象没有这个待遇了,分页查询查询结果就会有问题。

那么我们该怎么做呢 其实sqlSession.selectList这些方法最终的实现是

java 复制代码
public class DefaultSqlSession implements SqlSession {
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      dirty |= ms.isDirtySelect();
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  } 
  }

这里我们可以有个方法,那就是在executor进行真正的查询的时候把已经被封装成Param的参数给还原回去,那么怎么能做到呢,巧了,我们可以通过Interceptor拦截器来实现这个功能。 那我们怎么知道Interceptor是代理executor 执行器的呢。 MybatisSqlSessionFactoryBean类中有个方法

java 复制代码
    protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
        // ...
        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }
        // ...
    }

这里的interceptors就是我们可以注入的拦截器 而这些plugins是在哪使用的呢,我们知道每个sql执行都是通过SqlSession对象来执行的,而SqlSession对象是通过SqlSessionFactory对象来创建的,而SqlSessionFactory对象是通过MybatisSqlSessionFactoryBean类来创建的,而在这个类中我们看到有个方法

java 复制代码
    public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
                                                 boolean autoCommit) {
        Transaction tx = null;
        try {
            final Environment environment = configuration.getEnvironment();
            final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            final Executor executor = configuration.newExecutor(tx, execType);
            return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
            closeTransaction(tx); // may have fetched a connection so lets call close()
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
            ErrorContext.instance().reset();
        }
    }

    }

这个我们可以看出来每个Executor都是通过configuration.newExecutor方法创建的,而在MybatisConfiguration类中有个方法

java 复制代码
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    return (Executor) interceptorChain.pluginAll(executor);
}
java 复制代码
public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }

  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}
public interface Interceptor {

    Object intercept(Invocation invocation) throws Throwable;

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    default void setProperties(Properties properties) {
        // NOP
    }

}
java 复制代码
public class Plugin implements InvocationHandler {

    private final Object target;
    private final Interceptor interceptor;
    private final Map<Class<?>, Set<Method>> signatureMap;

    private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
            return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
        }
        return target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            Set<Method> methods = signatureMap.get(method.getDeclaringClass());
            if (methods != null && methods.contains(method)) {
                return interceptor.intercept(new Invocation(target, method, args));
            }
            return method.invoke(target, args);
        } catch (Exception e) {
            throw ExceptionUtil.unwrapThrowable(e);
        }
    }
}

很明显了,又是熟悉的配方,JDK动态代理,Plugin类就是用来生成动态代理的类。真正的查询执行之前都要通过interceptor.intercept方法来执行的。 那我们就可以通过实现Interceptor接口来处理参数了,让真正的executor执行之前把参数还原回去。

我们知道Mybatis-plus提供了MybatisPlusInterceptor 这个拦截器类,这个类是用来处理分页等功能的,我们可以直接在这个类中处理参数。 我这边是继承了这个类,然后重写了intercept方法,没错我是叫MybatisPlusPlusInterceptor 哈哈哈,这里边主要是逻辑是 Object realParameter = ParamUtil.disassemParameter(parameter);这个方法,我判断参数中只有只有IPage类型和其他的任意类型,而且只有默认的arg0,arg1这种形式的参数才进行还原,其他的参数不处理。 然后在调用下边的executor.queryexecutor.update方法前把真正的参数传过去就行了。

java 复制代码
public class MybatisPlusPlusInterceptor extends MybatisPlusInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(MybatisPlusPlusInterceptor.class);

    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object[] args = invocation.getArgs();
        if (target instanceof Executor) {
            final Executor executor = (Executor) target;
            Object parameter = args[1];
            //
            Object realParameter = ParamUtil.disassemParameter(parameter);

            boolean isUpdate = args.length == 2;
            MappedStatement ms = (MappedStatement) args[0];
            if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                BoundSql boundSql;
                if (args.length == 4) {
                    boundSql = ms.getBoundSql(realParameter);
                } else {
                    // 几乎不可能走进这里面,除非使用Executor的代理对象调用query[args[6]]
                    boundSql = (BoundSql) args[5];
                }
                for (InnerInterceptor query : interceptors) {
                    if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                        return Collections.emptyList();
                    }
                    query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                }
                CacheKey cacheKey = executor.createCacheKey(ms, realParameter, rowBounds, boundSql);
                return executor.query(ms, realParameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else if (isUpdate) {
                for (InnerInterceptor update : interceptors) {
                    if (!update.willDoUpdate(executor, ms, realParameter)) {
                        return -1;
                    }
                    update.beforeUpdate(executor, ms, realParameter);
                }
            }
        } else {
            // StatementHandler
            final StatementHandler sh = (StatementHandler) target;
            // 目前只有StatementHandler.getBoundSql方法args才为null
            if (null == args) {
                for (InnerInterceptor innerInterceptor : interceptors) {
                    innerInterceptor.beforeGetBoundSql(sh);
                }
            } else {
                Connection connections = (Connection) args[0];
                Integer transactionTimeout = (Integer) args[1];
                for (InnerInterceptor innerInterceptor : interceptors) {
                    innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
                }
            }
        }
        return invocation.proceed();
    }
    }

在分页插件中我们也要把参数还原下

java 复制代码
public class PaginationInnerPlusInterceptor implements InnerInterceptor {

    /**
     * 这里进行count,如果count为0这返回false(就是不再执行sql了)
     */
    @Override
    public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        IPage<?> page = ParameterUtils.findPage(parameter).orElse(null);
        if (page == null || page.getSize() < 0 || !page.searchCount() || resultHandler != Executor.NO_RESULT_HANDLER) {
            return true;
        }

        Object realParameter = ParamUtil.disassemParameter(parameter);


        BoundSql countSql;
        MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
        if (countMs != null) {
            countSql = countMs.getBoundSql(realParameter);
        } else {
            countMs = buildAutoCountMappedStatement(ms);
            String countSqlStr = autoCountSql(page, boundSql.getSql());
            PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
            countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), realParameter);
            PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
        }

        CacheKey cacheKey = executor.createCacheKey(countMs, realParameter, rowBounds, countSql);
        List<Object> result = executor.query(countMs, realParameter, rowBounds, resultHandler, cacheKey, countSql);
        long total = 0;
        if (CollectionUtils.isNotEmpty(result)) {
            // 个别数据库 count 没数据不会返回 0
            Object o = result.get(0);
            if (o != null) {
                total = Long.parseLong(o.toString());
            }
        }
        page.setTotal(total);
        return continuePage(page);
    }
    }

把ParamUtil也贴一下

java 复制代码
public class ParamUtil {
    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(ParamUtil.class);

    private static final Pattern PATTERN = Pattern.compile("^arg\d+$|^param\d+$");

    private ParamUtil() {}

    public static Object disassemParameter(Object parameter) {
        try {
            if (!(parameter instanceof Map)) {
                return parameter;
            }

            // 如果只有一个map或者对象参数类型,另一个类型是分页IPage,那么真正的参数只有第一个
            Set<Class<?>> classList = new HashSet<>();

            Map<?, ?> paramMap = (Map<?, ?>) parameter;
            if (paramMap.isEmpty()) {
                return parameter;
            }

            boolean b = paramMap.keySet().stream().allMatch(k -> k instanceof String && PATTERN.matcher((String) k).matches());
            if (!b) {
                return parameter;
            }
            for (Object value : paramMap.values()) {
                if (null != value) {
                    classList.add(value.getClass());
                }
            }

            if (classList.size() == 2) {
                for (Class<?> clazz : classList) {
                    if (IPage.class.isAssignableFrom(clazz)) {
                        // 如果是分页类型,那么真正的参数只有另一个
                        for (Object value : ((Map<?, ?>) parameter).values()) {
                            if (! (value instanceof IPage)) {
                                return value;
                            }
                        }
                        break;

                    }

                }
            }



        } catch (Exception ex) {
            logger.error("MybatisPlusPlusInterceptor disassemParameter [{}] error", parameter, ex);
        }

        return parameter;

    }
}

然后使用我们这个MybatisPlusPlusInterceptor拦截器替换掉MybatisPlusInterceptor拦截器就行了,PaginationInnerPlusInterceptor也用我们自己的

java 复制代码
    @Bean
    @ConditionalOnMissingBean
    public MybatisPlusPlusInterceptor mybatisPlusPlusInterceptor() {
        MybatisPlusPlusInterceptor interceptor = new MybatisPlusPlusInterceptor();
        // 添加分页功能
        interceptor.addInnerInterceptor(new PaginationInnerPlusInterceptor());
        // TODO 其他功能自己添加
        return interceptor;
    }
复制代码
相关推荐
sensenlin914 天前
Mybatis中SQL全大写或全小写影响执行性能吗
数据库·sql·mybatis
BXCQ_xuan4 天前
软件工程实践四:MyBatis-Plus 教程(连接、分页、查询)
spring boot·mysql·json·mybatis
wuyunhang1234564 天前
Redis----缓存策略和注意事项
redis·缓存·mybatis
lunz_fly19924 天前
【源码解读之 Mybatis】【基础篇】-- 第2篇:配置系统深度解析
mybatis
森林-4 天前
MyBatis 从入门到精通(第一篇)—— 框架基础与环境搭建
java·tomcat·mybatis
森林-4 天前
MyBatis 从入门到精通(第三篇)—— 动态 SQL、关联查询与查询缓存
sql·缓存·mybatis
java干货4 天前
MyBatis 的“魔法”:Mapper 接口是如何找到并执行 SQL 的?
数据库·sql·mybatis
嬉牛5 天前
项目日志输出配置总结(多数据源MyBatis+Logback)
mybatis·logback
哈喽姥爷6 天前
Spring Boot--yml配置信息书写和获取
java·数据库·spring boot·mybatis