Spring Bean混用异步与同步方法时偶发ClassNotFoundException问题咨询
嗨,我来帮你分析下这个困扰你的偶发异常问题~
先还原下你的场景:
我有一个Spring Bean包含两个方法,一个标注了@Async是异步方法,另一个是普通同步方法。两个方法都在访问通过jaxws-maven-plugin生成的SOAP端点客户端。同步方法运行完全正常,但异步方法偶尔会抛出java.lang.ClassNotFoundException: com.sun.xml.ws.spi.ProviderImpl异常,而且这个问题是偶发的,不是每次都会出现。
附上你提供的示例代码:
@Service public class MyService { @Transactional public void createTask(URL url) { new TaskService(url).getTaskServicePort().createTask(); } @Async @Transactional public void getTask(URL url) { new TaskService(url).getTaskServicePort().getTask(); } }
你的疑问:
- 这是异步方法调用导致的问题吗?
- 为什么会偶尔出现这个ClassNotFoundException?
- 我原本期望两个方法都能正常运行不抛出异常。
补充信息:Java版本17,Spring Boot版本2.7.4
问题根源分析
这个问题确实和异步方法的执行机制直接相关,主要原因有这几点:
类加载器上下文差异
Spring的异步方法是通过TaskExecutor在独立的线程池中执行的,这些线程的类加载器可能和原请求线程(比如Tomcat的请求处理线程)的类加载器不一致。同步方法在原线程执行时,类加载器能正确加载com.sun.xml.ws.spi.ProviderImpl;但异步线程的类加载器可能没有包含这个类所在的依赖包,就会偶发触发加载失败。JAX-WS Provider的加载逻辑
从Java 11开始,JAX-WS已经完全从JDK中移除了,你需要引入外部的JAX-WS实现(比如Sun的JAX-WS RI)。但Spring异步线程的上下文初始化过程中,可能没有正确让类加载器识别到这个Provider实现,当JDK的ServiceLoader尝试查找JAX-WS Provider时,就会因为类加载器的偶发异常找不到目标类。依赖包的可见性问题
如果你的JAX-WS依赖是通过provided这类非全局可见的scope引入的,异步线程的类加载器可能无法访问到这些依赖,当线程池中的线程第一次初始化时,就可能出现类加载失败的情况。
解决方案建议
针对这些原因,给你几个可行的解决方向:
明确指定JAX-WS Provider
在项目的src/main/resources/META-INF/services/目录下创建javax.xml.ws.spi.Provider文件,文件内容直接写com.sun.xml.ws.spi.ProviderImpl。这样ServiceLoader就能直接定位到实现类,避免类加载器的查找问题。统一异步线程的类加载器
自定义Spring的TaskExecutor,强制异步线程使用当前应用的类加载器。示例代码如下:
@Configuration public class AsyncConfig implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(25); executor.setThreadNamePrefix("MyAsyncThread-"); // 给异步任务设置和原线程一致的类加载器 executor.setTaskDecorator(runnable -> { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); return () -> { Thread currentThread = Thread.currentThread(); ClassLoader originalClassLoader = currentThread.getContextClassLoader(); try { currentThread.setContextClassLoader(contextClassLoader); runnable.run(); } finally { currentThread.setContextClassLoader(originalClassLoader); } }; }); executor.initialize(); return executor; } }
- 调整JAX-WS依赖的scope
检查你的pom.xml(或build.gradle)中JAX-WS相关依赖的scope,确保是compile或者runtime,而不是provided。示例依赖配置:
<dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-rt</artifactId> <version>2.3.5</version> </dependency> <dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-api</artifactId> <version>2.3.5</version> </dependency>
- 复用SOAP客户端实例
你现在每次调用方法都new TaskService(url),会重复初始化客户端,加重类加载负担。可以考虑把TaskService或者对应的Port实例缓存起来,或者通过Spring注入的方式管理,减少类加载的次数,降低偶发问题的概率。
备注:内容来源于stack exchange,提问作者Pido

