一次某个接口修改上线新功能,突然发现另一个带分页功能的接口异常,后台的错误栈大概是这个样子:
### 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 容器中,情况有一些变化。
@SpringBootApplication()
@ComponentScan(basePackages = {"com.example"})
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
return dispatcherServlet;
}
@Bean
public ServletRegistrationBean api() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(DemoConfig.class);
dispatcherServlet.setApplicationContext(applicationContext);
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/abc/api/*", "/def/api/*");
servletRegistrationBean.setName("API111");
servletRegistrationBean.setLoadOnStartup(1);
return servletRegistrationBean;
}
这里声明了一个全新的AnnotationConfigWebApplicationContext
和 DispatcherServlet
。问题就在这里。
我们都知道 Spring 的核心的会有一个 IOC 容器。所有需要用到的对象,也就是 Bean 会被在容器内进行管理。所以初次启动的时候,这个容器会创建生成。之然会根据一系列 Bean 的定义进行 Bean 的创建、初始化等生命周期管理。在这个过程中,会判断如果启用了自动配置,会判断哪些AutoConfiguration 类可以使用,可以进行配置,这个过程是个查找 Candidates的过程。
所以在 @EnableAutoConfiguration
注解中,会包含一个AutoConfigurationImportSelector
,这个Selector就会判断哪些需要被配置,然后根据前面说的各种OnConditionBean
之类的条件判断
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return StringUtils.toStringArray(configurations);
}
前面一系列走完之后,到上面这个 Bean 的定义,发现又要生成一个新的容器,所以在流程走完启动 WebServer 的时候,会有一个refresh的过程,这个时候,发现Controller 这个类里也启用了AutoConfiguration,就再执行一次。
附两个两次解析AutoConfiguration的调用栈
初次启动加载
-
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at org.springframework.core.io.support.SpringFactoriesLoader.loadFactories(SpringFactoriesLoader.java:94)
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 <0x14d3> (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.Hello.XXXApplication.main(XXXApplication.java:34)
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那些事儿」
请留下你的足迹
我们一起「终身成长」