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

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

### 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 链应该是这样的,

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

那这是不是成了 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 定义和映射全貌的神器

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

源码|实战|成长|职场

这里是「Tomcat那些事儿」

请留下你的足迹

我们一起「终身成长」

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值