部署多个Spring Boot WAR包至Tomcat时出现异常问题排查
从你描述的问题和异常信息来看,核心问题是java.lang.ClassCastException: com.sun.proxy.$Proxy227 cannot be cast to org.springframework.security.web.authentication.WebAuthenticationDetailsSource,这是典型的类加载器冲突导致的问题——同一个类被不同的类加载器加载后,JVM会判定它们是完全不同的类型,自然无法完成类型转换。下面是具体的排查步骤和解决方案:
1. 统一两个项目的Spring Boot(及Spring Security)版本
Spring Boot的starter依赖会统一管理所有子依赖的版本,如果两个项目使用不同的Spring Boot版本,对应的Spring Security版本必然存在差异。当Tomcat同时部署多个WAR包时,类加载器可能会优先加载其中一个版本的类,导致另一个项目的代码无法兼容。
- 解决方法:
打开两个项目的pom.xml,确保spring-boot-starter-parent的版本完全一致:
可以执行<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.14</version> <!-- 替换为你实际使用的稳定版本,两个项目必须保持一致 --> <relativePath/> </parent>mvn dependency:tree命令,分别检查两个项目的Spring Security依赖版本,确认没有版本差异。
2. 清理Tomcat全局lib目录的Spring相关依赖
如果Tomcat的$CATALINA_HOME/lib目录下手动放置了Spring、Spring Security等JAR包,这些类会被Tomcat的父类加载器加载,所有WAR包都会共享这些类。当项目内的依赖版本和全局版本不一致时,就会出现类型不兼容的问题。
- 解决方法:
检查Tomcat的lib目录,删除所有Spring相关的JAR包(包括spring-core、spring-security-web等),让每个WAR包使用自己打包进去的依赖(Spring Boot打包WAR时默认会包含所有依赖,除非你配置了<scope>provided</scope>)。
3. 检查Spring Boot WAR打包的配置正确性
确保两个项目都按照Spring Boot WAR部署的规范配置:
(1)启动类继承SpringBootServletInitializer
每个项目的启动类必须继承该类并重写configure方法,确保Tomcat能正确初始化Spring上下文:
@SpringBootApplication public class YourProjectApplication extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(YourProjectApplication.class); } public static void main(String[] args) { SpringApplication.run(YourProjectApplication.class, args); } }
(2)正确配置Tomcat依赖的scope
在pom.xml中设置spring-boot-starter-tomcat的scope为provided,避免打包到WAR中与Tomcat自带的Tomcat类冲突:
<packaging>war</packaging> <dependencies> <!-- 其他业务依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> </dependencies>
4. 检查Spring Security配置的独立性
如果其中一个项目自定义了WebAuthenticationDetailsSource或者相关的安全Bean,并且Tomcat的上下文配置了交叉上下文(crossContext),可能会导致Bean被跨WAR共享,引发冲突。
- 解决方法:
- 确保每个项目的Spring Security配置是完全独立的,不要将安全相关的Bean暴露到全局上下文。
- 检查Tomcat的
server.xml,确认<Context>标签的crossContext属性为false(默认值),避免WAR之间共享上下文:<Context docBase="your-war-name" path="/your-path" crossContext="false"/>
5. 验证类加载器隔离情况
如果以上步骤都没解决问题,可以添加一个简单的类加载器检查组件,查看WebAuthenticationDetailsSource的加载来源:
@Component public class ClassLoaderDebugger implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { Class<?> detailsSourceClass = org.springframework.security.web.authentication.WebAuthenticationDetailsSource.class; System.out.println("[" + event.getApplicationContext().getDisplayName() + "] WebAuthenticationDetailsSource loaded by: " + detailsSourceClass.getClassLoader()); System.out.println("[" + event.getApplicationContext().getDisplayName() + "] Current context class loader: " + Thread.currentThread().getContextClassLoader()); } }
部署两个WAR后查看日志,如果两个项目的WebAuthenticationDetailsSource被不同的类加载器加载,但仍然出现转换异常,说明可能存在某个第三方依赖强制使用了父类加载器加载Spring类,需要进一步排查依赖树。
内容的提问来源于stack exchange,提问作者Arya

