前端时间把老系统的代码迁移到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);


// 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扫码接口的,结果并不是,打了断点也没有,这边重点的就是MybatisSqlSessionFactoryBean
和MybatisConfiguration
这两个类,这个更换后就会把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中,这里边的configuration
是MybatisConfiguration
类
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方法注册的。最后调用的MybatisMapperAnnotationBuilder
的parse
方法解析注解生成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动态代理生成的。 当然这边的configuration
是MybatisConfiguration
类,这个类是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.query
和executor.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;
}