在 还在给每个请求加前缀避免模块间接口冲突呢? 中,我们讨论了在一个 Spring Boot 应用中注册多个 DispatcherServlet 来实现应用上下文隔离的方案,以达到在不同 Servlet 关联的上下文中,注册相同 RequestMapping 的 Controller,乃至相同名称的 Bean 的效果。
在实际使用这种模式时,可能会遇到某些原因导致上下文隔离的效果跟预期不一致的情况,比如 SpringBootApplication 启动类上使用了 @ComponentScan 注解,导致某些 Bean 被注册到了多个上下文中,从而引发一些奇怪的问题。
为了排查这些问题,我们需要找到应用中所有的 BeanFactory,以及它们各自注册了哪些 Bean。
本文仍以 multi-dispatcher demo 工程为例,给出通过 debug 方式加入的断点位置,以观察所有的 BeanFactory 及 ApplicationContext。
断点行数以 Spring Boot 2.2.2.RELEASE 和 Spring Framework 5.2.2.RELEASE 为例,不同版本可能会有差异。
根 ApplicationContext 及 BeanFactory
通常,Spring Boot 应用启动时,会包含如下 main 方法:
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
run 方法返回的 ConfigurableApplicationContext 就是根 ApplicationContext:
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args)
根 BeanFactory 保存在 Spring Context 中提供的 GenericApplicationContext 类的 beanFactory 字段中:
private final DefaultListableBeanFactory beanFactory;
断点:SpringApplication:311
我们可以在 SpringApplication.run 方法中调用 createApplicationContext 方法处加入断点:
context = createApplicationContext();
断点:SpringApplication:588
createApplicationContext 方法会根据 Web 应用类型初始化不同的 ApplicationContext。对于本例中的 Servlet Web 应用来说,通常会返回 AnnotationConfigServletWebServerApplicationContext:
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
断点:ClassPathBeanDefinitionScanner:273
ClassPathBeanDefinitionScanner 类的 doScan 方法可以观察到当前初始化的 BeanFactory 会扫描的基础包:
protected Set<BeanDefinitionHolder> doScan(String... basePackages)
断点:DefaultListableBeanFactory:853
DefaultListableBeanFactory 类的 preInstantiateSingletons 方法可以观察到当前 BeanFactory 中包含的 Bean 定义,preInstantiateSingletons 方法会在 BeanFactory 创建的最终阶段被调用:
// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
每个 Servlet 关联的 WebApplicationContext 及 BeanFactory
断点:FrameworkServlet:530
在 DispatcherServlet 的父类 FrameworkServlet 的 initServletBean 方法中,会对 Servlet 关联的 WebApplicationContext 进行初始化:
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
断点:AbstractRefreshableApplicationContext:130
AbstractRefreshableApplicationContext 类的 refreshBeanFactory 方法会创建 WebApplicationContext 的 BeanFactory:
/**
* This implementation performs an actual refresh of this context's underlying
* bean factory, shutting down the previous bean factory (if any) and
* initializing a fresh bean factory for the next phase of the context's lifecycle.
*/
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
Demo 应用中的 BeanFactory 和 ApplicationContext 列表
| Root | BeanFactory | ApplicationContext |
|---|---|---|
| ID | application | org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6e01f9b0 |
| ObjectId | @3157 | @3149 |
| Parent | null | null |
| Bar Servlet | BeanFactory | ApplicationContext |
|---|---|---|
| ID | org.springframework.web.context.WebApplicationContext:/Bar servlet | org.springframework.web.context.WebApplicationContext:/Bar servlet |
| ObjectId | @5578 | @5591 |
| Parent | @3157 | @3149 |
| Foo Servlet | BeanFactory | ApplicationContext |
|---|---|---|
| ID | org.springframework.web.context.WebApplicationContext:/Foo servlet | org.springframework.web.context.WebApplicationContext:/Foo servlet |
| ObjectId | @5778 | @5777 |
| Parent | @3157 | @3149 |