mybatis 分页_为什么SpringBoot 要两次扫描包?一个MyBatis 分页插件异常问题分析

7054719c273b1a2874e07bd3628c6c07.png

一次某个接口修改上线新功能,突然发现另一个带分页功能的接口异常,后台的错误栈大概是这个样子:

### Error querying database. Cause: java.lang.RuntimeException: 在系统中发现了多个分页插件,请检查系统配置!

### Cause: java.lang.RuntimeException: 在系统中发现了多个分页插件,请检查系统配置!

at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) ~[mybatis-3.4.5.jar:3.4.5]

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:150) ~[mybatis-3.4.5.jar:3.4.5]

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141) ~[mybatis-3.4.5.jar:3.4.5]

at sun.reflect.GeneratedMethodAccessor237.invoke(Unknown Source) ~[na:na]

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_192]

at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_192]

at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433) ~[mybatis-spring-1.3.1.jar:1.3.1]

... 108 common frames omitted

Caused by: java.lang.RuntimeException: 在系统中发现了多个分页插件,请检查系统配置!

at com.github.pagehelper.PageHelper.skip(PageHelper.java:56) ~[pagehelper-5.1.6.jar:na]

at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:93) ~[pagehelper-5.1.6.jar:na]

at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61) ~[mybatis-3.4.5.jar:3.4.5]

at com.sun.proxy.$Proxy193.query(Unknown Source) ~[na:na]

at com.github.pagehelper.util.ExecutorUtil.executeAutoCount(ExecutorUtil.java:138) ~[pagehelper-5.1.6.jar:na]

at com.github.pagehelper.PageInterceptor.count(PageInterceptor.java:148) ~[pagehelper-5.1.6.jar:na]

at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:97) ~[pagehelper-5.1.6.jar:na]

at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61) ~[mybatis-3.4.5.jar:3.4.5]

at com.sun.proxy.$Proxy193.query(Unknown Source) ~[na:na]

at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148) ~[mybatis-3.4.5.jar:3.4.5]

这个时候第一反应是怀疑是不是代码里真的添加了多次分页插件的 maven 依赖。仔细看了一遍代码里的依赖,发现并没有。

而异常产生的原因,是Controller里被增加了一个 @EnableAutoConfiguration的注解。

这也能影响到分页插件?

那只能从异常这里入手了。最顶部的异常是从skip方法开始的,我们来看这个方法:

我们来看这个方法:

public boolean skip(MappedStatement ms, Object parameterObject, RowBounds rowBounds) {

if (ms.getId().endsWith("_COUNT")) {

throw new RuntimeException("在系统中发现了多个分页插件,请检查系统配置!");

} else {

Page page = this.pageParams.getPage(parameterObject, rowBounds);

if (page == null) {

return true;

} else {

if (StringUtil.isEmpty(page.getCountColumn())) {

page.setCountColumn(this.pageParams.getCountColumn());

}

this.autoDialect.initDelegateDialect(ms);

return false;

}

}

}

哦,ms.getId().endsWith("_COUNT") 这个时候会抛异常,什么情况?

因为分页的请求,需要判断总记录数,以及 pageSize, pageNum 这些情况,这里是在处理分页情况,计算总记录数。底层是通过 MyBatis 的一个 PageInterceptor 来实现的。Debug 一会代码后就会发现,这个异常出现,是因为调用链里有多个 PageInterceptor 导致的。

什么原因

回过头来看看,为什么会出现多个PageInterceptor?

正常情况下,这个Interceptor是在PageHelperAutoConfiguration 里注册的,一般情况 Interceptor 链应该是这样的,

c8c8922887b8934e3921608351d0c940.png

但在异常情况下,变成这样的:

4b65a72ff30cfad46f4af03934185892.png

那这是不是成了 AutoConfiguration  的锅呢?

Auto-Configuration

这事要从 Spring Boot 的 Auto-configuration 机制说起。

如果经历过 Spring Boot 出现之前 Spring 开发,那一定记忆犹新。

开发过程中,需要进行大量的配置,包扫描路径,事务,视图处理器等等,对于快速上手开发还是有一定的门槛的。

而 Spring Boot 的出现,带来了 AutoConfiguration。
虽然本质上还是 Spring 4 提供的这种在一个类中通过 @Configuration 来声明 各种 @Bean 的 JavaConfig,但有了 SPI 的能力,可以在运行时根据 classpath中的依赖,来判断都需要加载哪些配置。

这种 Auto 的能力,背后还是靠  。他需要猜都需要帮我们做哪些配置来满足应用的功能。而猜靠什么?靠的是各种条件的判断。
SPI的方式是说在运行时,我们加载的classpath下所有 包含 META-INF/spring.factories的 Jar 文件。

以这次的主角 PageHelper为例,Jar 文件里的这个文件,内容是这个样子的:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

com.github.pagehelper.autoconfigure.PageHelperAutoConfiguration

SpringBoot 会明白从哪对其进行自动配置。

来看这个类的内容:

@Configuration

@ConditionalOnBean(SqlSessionFactory.class)

@EnableConfigurationProperties(PageHelperProperties.class)

@AutoConfigureAfter(MybatisAutoConfiguration.class)

public class PageHelperAutoConfiguration {

@Autowired

private List<SqlSessionFactory> sqlSessionFactoryList;

@Autowired

private PageHelperProperties properties;

/**

* 接受分页插件额外的属性

*

* @return

*/

@Bean

@ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)

public Properties pageHelperProperties() {

return new Properties();

}

@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.getConfiguration().addInterceptor(interceptor);

}

}

}

注意这里的@ConditionalOnBean @AutoConfigureAfter,这些标识出在什么条件下,什么顺序下这个配置生效。这里的是需要有SqlSessionFactory 的 Bean 存在,同时在MybatisAutoConfiguration之后再进行配置。

@Configuration

@ConditionalOnBean(SqlSessionFactory.class)

@EnableConfigurationProperties(PageHelperProperties.class)

@AutoConfigureAfter(MybatisAutoConfiguration.class)

这些 AutoConfiguration 一般通过 @EnableAutoConfiguration 来启用,也可以手动关闭。由于自动配置是个常用的操作,所以集成到了 SpringBoot 的常用注解@SpringBootApplication里。

@SpringBootConfiguration

@EnableAutoConfiguration

@ComponentScan(excludeFilters = {

@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),

@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

public @interface SpringBootApplication {

默认在主类中使用这个注解时已经启动了自动配置。

而要添加多个 PageInterceptor,一定时这个方法被执行了多次。这又是为什么?

为什么会执行多次呢?

还记得咱们开头说问题产生的时候,这次有一个操作是在某个 Controller 的声明里增加了一句:@EnableAutoConfiguration
大概是这个样子

@Controller

@RequestMapping(value = "xxx", method = {RequestMethod.POST, RequestMethod.GET})

@EnableAutoConfiguration

@Slf4j

public class XXXTestAPI {

}

这一行代码,会导致项目里能够识别出来的 AutoConfiguration 类都会被执行一次。前面说过 @SpringBootApplication 注解默认包含了启用功能,启动的时候会执行一次,加上这里的,就多次了嘛。

为什么加个注解就要再执行一次自动配置呢?这么不智能?那是不是所有的Controller里都加上,就得启动执行几十次呢?

不是,这还真怪不着 SpringBoot。

我们在主类中定义了这样一个Bean,会在运行时注册一个 Servlet。虽说是 Servlet 3.0 的一个Dynamiic Servlet 的特性,但因为我们在 Spring 容器中,情况有一些变化。

  1. @SpringBootApplication()

  2. @ComponentScan(basePackages = {"com.example"})

  3. public class DemoApplication {

  4. public static void main(String[] args) {

  5. SpringApplication.run(DemoApplication.class, args);

  6. }

  7. @Bean

  8. public DispatcherServlet dispatcherServlet() {

  9. DispatcherServlet dispatcherServlet = new DispatcherServlet();

  10. return dispatcherServlet;

  11. }

  12. @Bean

  13. public ServletRegistrationBean api() {

  14. DispatcherServlet dispatcherServlet = new DispatcherServlet();

  15. AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();

  16. applicationContext.register(DemoConfig.class);

  17. dispatcherServlet.setApplicationContext(applicationContext);

  1. ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/abc/api/*", "/def/api/*");

  2. servletRegistrationBean.setName("API111");

  3. servletRegistrationBean.setLoadOnStartup(1);

  4. return servletRegistrationBean;

  5. }

这里声明了一个全新的AnnotationConfigWebApplicationContext 和 DispatcherServlet。问题就在这里。

我们都知道 Spring 的核心的会有一个 IOC 容器。所有需要用到的对象,也就是 Bean 会被在容器内进行管理。所以初次启动的时候,这个容器会创建生成。之然会根据一系列 Bean 的定义进行 Bean 的创建、初始化等生命周期管理。在这个过程中,会判断如果启用了自动配置,会判断哪些AutoConfiguration 类可以使用,可以进行配置,这个过程是个查找 Candidates的过程。

所以在 @EnableAutoConfiguration注解中,会包含一个AutoConfigurationImportSelector,这个Selector就会判断哪些需要被配置,然后根据前面说的各种OnConditionBean之类的条件判断

  1. public String[] selectImports(AnnotationMetadata annotationMetadata) {

  2. if (!isEnabled(annotationMetadata)) {

  3. return NO_IMPORTS;

  4. }

  5. AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader

  6. .loadMetadata(this.beanClassLoader);

  7. AnnotationAttributes attributes = getAttributes(annotationMetadata);

  8. List<String> configurations = getCandidateConfigurations(annotationMetadata,

  9. attributes);

  10. configurations = removeDuplicates(configurations);

  11. Set<String> exclusions = getExclusions(annotationMetadata, attributes);

  12. checkExcludedClasses(configurations, exclusions);

  13. configurations.removeAll(exclusions);

  14. configurations = filter(configurations, autoConfigurationMetadata);

  15. fireAutoConfigurationImportEvents(configurations, exclusions);

  16. return StringUtils.toStringArray(configurations);

  17. }

前面一系列走完之后,到上面这个 Bean 的定义,发现又要生成一个新的容器,所以在流程走完启动 WebServer 的时候,会有一个refresh的过程,这个时候,发现Controller 这个类里也启用了AutoConfiguration,就再执行一次。

附两个两次解析AutoConfiguration的调用栈

初次启动加载

    1. "main@1" prio=5 tid=0x1 nid=NA runnable

    2. java.lang.Thread.State: RUNNABLE

    3. at org.springframework.core.io.support.SpringFactoriesLoader.loadFactories(SpringFactoriesLoader.java:94)

    4. at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getAutoConfigurationImportFilters(AutoConfigurationImportSelector.java:266)

    5. at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.filter(AutoConfigurationImportSelector.java:237)

    6. at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.selectImports(AutoConfigurationImportSelector.java:102)

    7. at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector$AutoConfigurationGroup.process(AutoConfigurationImportSelector.java:386)

    8. at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGrouping.getImports(ConfigurationClassParser.java:828)

    9. at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:563)

    10. at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:188)

    11. at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:316)

    12. at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233)

    13. at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:271)

    14. at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:91)

    15. at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:692)

    16. at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:530)

    17. - locked <0x14d3> (a java.lang.Object)

    18. at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)

    19. at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)

    20. at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:386)

    21. at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)

    22. at org.springframework.boot.SpringApplication.run(SpringApplication.java:1242)

    23. at org.springframework.boot.SpringApplication.run(SpringApplication.java:1230)

    24. at com.xxx.test.Hello.XXXApplication.main(XXXApplication.java:34)

  1. WebServer启动时加载

"main@1" prio=5 tid=0x1 nid=NA runnable

java.lang.Thread.State: RUNNABLE

at org.springframework.core.io.support.SpringFactoriesLoader.loadFactories(SpringFactoriesLoader.java:89)

at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.getAutoConfigurationImportFilters(AutoConfigurationImportSelector.java:266)

at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.filter(AutoConfigurationImportSelector.java:237)

at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.selectImports(AutoConfigurationImportSelector.java:102)

at org.springframework.boot.autoconfigure.AutoConfigurationImportSelector$AutoConfigurationGroup.process(AutoConfigurationImportSelector.java:386)

at org.springframework.context.annotation.ConfigurationClassParser$DeferredImportSelectorGrouping.getImports(ConfigurationClassParser.java:828)

at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:563)

at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:188)

at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:316)

at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233)

at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:271)

at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:91)

at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:692)

at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:530)

- locked <0x6684> (a java.lang.Object)

at org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(FrameworkServlet.java:677)

at org.springframework.web.servlet.FrameworkServlet.initWebApplicationContext(FrameworkServlet.java:544)

at org.springframework.web.servlet.FrameworkServlet.initServletBean(FrameworkServlet.java:502)

at org.springframework.web.servlet.HttpServletBean.init(HttpServletBean.java:172)

at javax.servlet.GenericServlet.init(GenericServlet.java:158)

at org.apache.catalina.core.StandardWrapper.initServlet(StandardWrapper.java:1144)

- locked <0x65de> (a org.apache.catalina.core.StandardWrapper)

at org.apache.catalina.core.StandardWrapper.load(StandardWrapper.java:986)

at org.apache.catalina.core.StandardContext.loadOnStartup(StandardContext.java:4978)

at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedContext.deferredLoadOnStartup(TomcatEmbeddedContext.java:80)

at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.performDeferredLoadOnStartup(TomcatWebServer.java:284)

at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:201)

- locked <0x66a0> (a java.lang.Object)

at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.startWebServer(ServletWebServerApplicationContext.java:311)

at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:164)

at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)

- locked <0x3350> (a java.lang.Object)

at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)

at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)

at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:386)

at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)

at org.springframework.boot.SpringApplication.run(SpringApplication.java:1242)

at org.springframework.boot.SpringApplication.run(SpringApplication.java:1230)

at com.xxx.test.XXXApplication.main(XXXApplication.java:34)

解决方式

解决这个问题,想到的方式有以下几种,欢迎补充:

方式一:
可以把新加的这个注解去掉,而这个注解本质是让项目里识别SPI方式提供的其他AutoConfiguration类加载,那些@Configuration能根据条件识别出来。我们项目里不需要再声明。

方式二:
在主类中声明的定义DispatcherServlet的Bean, 可以将 DispatcherServlet 定义成全局的,类似这样:

@Bean

public DispatcherServlet dispatcherServlet() {

DispatcherServlet dispatcherServlet = new DispatcherServlet();

return dispatcherServlet;

}

这样后续注册的也都在同一个IOC容器中,不会再次触发自动配置。

方式三:

主类中定义的这个Bean, 如果只为了修改请求路径的话,定义 Context-path就足够了, 在applicaton.properties 可以通过server.servlet.context-path=/xxx/api 的配置来实现。

相关阅读

如何开发自己的Spring Boot Starter

怎样阅读源代码?

如何给Spring Boot 的嵌入式 Tomcat 部署多个应用?

Spring Boot Admin (一) 请求处理原理

一览Spring Bean 定义和映射全貌的神器

更多常见问题,请关注公众号,在菜单「常见问题」中查看,也欢迎加我微信,一起交流。 

6aa36162f70f7138b3678eb0d740f671.png

01af081610a64780d7e6ed07a8478787.png

源码|实战|成长|职场

这里是「Tomcat那些事儿」

请留下你的足迹

我们一起「终身成长」

051d9f4ee653cc65e017a24cd4e5d0ee.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值