如何让Spring MVC仅创建一个Spring上下文?消除多上下文导致的Bean重复问题
这种情况我之前帮不少开发者排查过,确实挺头疼的——双倍的Bean实例和异步线程不仅会白白消耗服务器资源,还可能引发各种诡异的并发问题(比如重复执行定时任务、数据不一致等)。结合你的场景,核心问题是Spring MVC的根WebApplicationContext和DispatcherServlet的WebApplicationContext加载了重复的Bean定义,导致所有组件都被实例化了两次。下面我给你拆解常见原因和对应的解决办法:
一、最常见的坑:配置文件重复加载
Spring MVC中,ContextLoaderListener负责初始化根上下文(全局共享的上下文),而DispatcherServlet会初始化自己的MVC专用上下文(是根上下文的子上下文)。如果两者的contextConfigLocation指向了同一个/重叠的XML配置文件,就会导致所有Bean被加载两次。
错误示例(web.xml)
<!-- 根上下文加载applicationContext.xml --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <!-- DispatcherServlet又加载了同一个applicationContext.xml --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </init-param> </servlet>
解决办法:拆分配置文件
- 根上下文:负责加载业务逻辑、数据源、异步任务、定时任务等全局Bean(比如
applicationContext.xml里放<context:component-scan base-package="com.example.service,com.example.repository"/>、数据源配置、@Async相关的@EnableAsync等)。 - MVC上下文:只加载Web层专属Bean(比如新建
spring-mvc.xml,里面放<context:component-scan base-package="com.example.controller"/>、视图解析器、拦截器、@EnableWebMvc等)。
调整后的web.xml示例:
<listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring-mvc.xml</param-value> </init-param> </servlet>
二、注解扫描范围重叠(基于JavaConfig的场景)
如果你的项目用Java配置类代替XML,很容易犯的错误是根上下文和MVC上下文的@ComponentScan范围重复,导致所有@Service、@Component、@Async标注的Bean被实例化两次。
错误示例
// 根上下文配置类(被ContextLoaderListener加载) @Configuration @ComponentScan("com.example") // 扫描整个项目包,包含Controller和Service public class RootConfig {} // MVC配置类(被DispatcherServlet加载) @Configuration @EnableWebMvc @ComponentScan("com.example") // 再次扫描整个包,重复实例化Bean public class WebMvcConfig {}
解决办法:精准拆分扫描范围
两种方式任选其一:
- 按包拆分:根上下文扫业务层,MVC上下文只扫控制器层
// 根上下文:扫Service、Repository等 @Configuration @ComponentScan(basePackages = {"com.example.service", "com.example.repository"}) @EnableAsync // 异步任务配置放在根上下文 public class RootConfig {} // MVC上下文:只扫Controller @Configuration @EnableWebMvc @ComponentScan(basePackages = "com.example.controller") public class WebMvcConfig {}
- 用过滤规则拆分:根上下文排除
@Controller,MVC上下文只包含@Controller
// 根上下文:排除Controller注解的类 @Configuration @ComponentScan(basePackages = "com.example", excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)) @EnableAsync public class RootConfig {} // MVC上下文:只包含Controller注解的类,关闭默认扫描规则 @Configuration @EnableWebMvc @ComponentScan(basePackages = "com.example", includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class), useDefaultFilters = false) public class WebMvcConfig {}
三、代码中手动创建了额外的ApplicationContext
还有一种可能:你在业务代码里手动调用了new ClassPathXmlApplicationContext(...)或者AnnotationConfigApplicationContext(...),导致额外的Spring上下文被初始化,和容器本身的上下文重复。
排查办法
全局搜索代码中所有创建ApplicationContext实例的地方,确保只有Spring的ContextLoaderListener和DispatcherServlet负责上下文初始化,不要手动创建上下文实例。
验证问题是否解决
可以通过以下方式确认:
- 在关键Bean的构造方法中添加日志(比如
System.out.println("Bean实例化:" + this.hashCode())),启动应用后看是否打印两次相同Bean的日志。 - 用JVM监控工具(比如VisualVM、JConsole)查看Bean实例的数量,确认是否恢复到预期的数量。
- 查看异步线程池的线程数量,确认不再翻倍。
内容的提问来源于stack exchange,提问作者Dasmowenator

