前言:懒惰的我在项目中配置的mybatis-plus PaginationInterceptor分页插件突然失效了,在网上搜了大量文章还是没找到根本原因,只能把mybatis插件加载源码一撸到底了。
1、Mybatis-plus PaginationInterceptor加载原理源码解析
mybatis-plus PaginationInterceptor加载顺序:
源码分析:
1)MybatisPlusAutoConfiguration类,主要用来自动装配实例化SqlSessionFactory类对象
关键方法:sqlSessionFactory实例化sqlSessionFactory并加载所有mybatis plugins插件(包括分页page插件)
黄色代码为把所有mybatis plugins插件装配到sqlSessionFactory中。
注意:该实例化方法上添加了@ConditionalOnMissingBean注解,表示只有上下文中没有实例化sqlSessionFactory才会执行,所以当现有系统中自定义了sqlSessionFactory实例化方法,则该方法不会执行。
Java
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// TODO使用 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()));
}
applyConfiguration(factory);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
//此处省略一万行代码....
return factory.getObject();
}
2)MybatisSqlSessionFactoryBean类:sqlSessionFactory的工厂类,实际去构建sqlSessionFactory对象和加载plus插件,主要方法代码如下:该方法主要调targetConfiguration.addInterceptor(plugin)把所有已经实例化的mybatis插件装载进MybatisConfiguration类中,MybatisConfiguration类主要是承载了创建SqlSessionFactory对象的所有上下文配置信息。
Java
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final MybatisConfiguration targetConfiguration;
//此处省略一万行代码...
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
//此处省略一万行代码...
return sqlSessionFactory;
}
3) MybatisConfiguration类继承Configuration类:SqlSessionFactory对象的所有上下文配置信息。该类主要提供了PaginationInterceptor插件的添加和使用等方法:
Java
//添加拦截器
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
Java
//执行sql时,先执行所有sqlSessionFactory的拦截器插件
@Override
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new MybatisBatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new MybatisReuseExecutor(this, transaction);
} else {
executor = new MybatisSimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new MybatisCachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
4)InterceptorChain类:这个类用于存储拦截器对象list,并提供添加和调用的方法
Java
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
//依次调用拦截器方法,注意这里时for循环调用,即,拦截器调用是有先后顺序的。
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);
}
}
到这里整个mybatis-plus插件的整个加载过程就已经结束了,我们page分页失效的问题也找到了,因为我们新增了自定义的sqlSessionFactory实例化类,所以没有走到MybatisAutoConfigin类去创建sqlSessionFactory对象了,从而也不存在加载page插件这么一回事了。解决方案如下:
Java
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, @Autowired PaginationInterceptor paginationInterceptor) throws Exception {
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(DefaultDataSourceConfig.MAPPER_LOCATION);
final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(resources);
//手动把page分页插件加载到sesionFactory中去
sessionFactory.setPlugins(paginationInterceptor);
//开启驼峰映射DTO
Objects.requireNonNull(sessionFactory.getObject()).getConfiguration().setMapUnderscoreToCamelCase(true);
return sessionFactory.getObject();
}
值得注意的一点是,mybatis-plus sqlSessionFactory级别插件的加载是跟sqlSessionFactory的自动装备强耦合在一起的,从而会导致其他自定义sqlSessionFactory不能自动装配mybatis-plus的插件,需要在自定义sqlSessionFactory中手动添加。改进方案其实可以借鉴Pagehepler分页插件的加载实现原理,使插件和sesionFactory的创建结偶,这样自定义sqlSessionFactory实例化类也能支持自动装载插件了。
附上PageHelper插件装载源码:
Java
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {
//获取到所有sqlSessionFactory list
@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;
@Autowired
private PageHelperProperties properties;
/**
*接受分页插件额外的属性
*
* @return
*/
@Bean
@ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
public Properties pageHelperProperties() {
return new Properties();
}
//装载pageHepler插件
@PostConstruct
public void addPageInterceptor() {
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
//先把一般方式配置的属性放进去
properties.putAll(pageHelperProperties());
//在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
properties.putAll(this.properties.getProperties());
interceptor.setProperties(properties);
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
//对所有的sqlSessionFactory装载pagehepler插件
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
2、Mybatis-plus sqlSessionFactory执行插件源码解析
前面忽略一万步,感兴趣的同学可以自行沿着插件执行的源码往上扩展查看整个sqlSessionFactory是如何执行的,这里我们主要看sqlSessionFactory执行sql时,如何去执行插件的这一块
主要类及方法:
MybatisConfiguration:sqlSessionFactory上下文类。前文其实已经有提到了,该类主要提供了插件的添加和使用的方法。在sqlSessionFactory执行sql时,会调用newExecutor方法来调用依次调用所有的插件。
Java
//执行sql时,先执行所有sqlSessionFactory的拦截器插件
@Override
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new MybatisBatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new MybatisReuseExecutor(this, transaction);
} else {
executor = new MybatisSimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new MybatisCachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain类:pluginAll方法依次去调用了所有拦截器的plugin方法
Java
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
//依次调用拦截器方法,注意这里时for循环调用,即,拦截器调用是有先后顺序的。
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
//依次调用拦截器的plugin方法
target = interceptor.plugin(target);
}
return target;
}
//添加拦截器
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
//获取所有拦截器
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
这里我们还是围绕着PaginationInterceptor分页插件来看具体调用
PaginationInterceptor类:mybatis-plus分页插件类
主要方法:
生成代理类
Java
@Override
public Object plugin(Object target) {
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
}
return target;
}
TypeScript
//生成代理类
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)) {
//执行拦截器中的intercept方法
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
Java
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// SQL解析
this.sqlParser(metaObject);
//先判断是不是SELECT操作 (2019-04-10 00:37:31 跳过存储过程)
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
|| StatementType.CALLABLE == mappedStatement.getStatementType()) {
return invocation.proceed();
}
//针对定义了rowBounds,做为mapper接口方法的参数
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
Object paramObj = boundSql.getParameterObject();
//判断参数里是否有page对象
IPage<?> page = null;
if (paramObj instanceof IPage) {
page = (IPage<?>) paramObj;
} else if (paramObj instanceof Map) {
for (Object arg : ((Map<?, ?>) paramObj).values()) {
if (arg instanceof IPage) {
page = (IPage<?>) arg;
break;
}
}
}
/*
*不需要分页的场合,如果 size 小于 0 返回结果集
*/
if (null == page || page.getSize() < 0) {
return invocation.proceed();
}
if (this.limit > 0 && this.limit <= page.getSize()) {
//处理单页条数限制
handlerLimit(page);
}
String originalSql = boundSql.getSql();
Connection connection = (Connection) invocation.getArgs()[0];
if (page.isSearchCount() && !page.isHitCount()) {
SqlInfo sqlInfo = SqlParserUtils.getOptimizeCountSql(page.optimizeCountSql(), countSqlParser, originalSql);
this.queryTotal(sqlInfo.getSql(), mappedStatement, boundSql, page, connection);
if (page.getTotal() <= 0) {
return null;
}
}
DbType dbType = Optional.ofNullable(this.dbType).orElse(JdbcUtils.getDbType(connection.getMetaData().getURL()));
IDialect dialect = Optional.ofNullable(this.dialect).orElse(DialectFactory.getDialect(dbType));
String buildSql = concatOrderBy(originalSql, page);
DialectModel model = dialect.buildPaginationSql(buildSql, page.offset(), page.getSize());
Configuration configuration = mappedStatement.getConfiguration();
List<ParameterMapping> mappings = new ArrayList<>(boundSql.getParameterMappings());
Map<String, Object> additionalParameters = (Map<String, Object>) metaObject.getValue("delegate.boundSql.additionalParameters");
model.consumers(mappings, configuration, additionalParameters);
metaObject.setValue("delegate.boundSql.sql", model.getDialectSql());
metaObject.setValue("delegate.boundSql.parameterMappings", mappings);
return invocation.proceed();
}