spire 注入SPI。JDK的CharsetProvider 的契约要求实现线程安全(JDK 可能在任意线程并发调用),Spire 违反了这个原则。
最早发现是RestTemplate并发请求报错。
java.lang.ArrayIndexOutOfBoundsException
at java.util.ArrayList.add
at com.spire.doc.packages.sprstn.spr▧┼(CharsetProvider.java:48) ← 元凶
at java.nio.charset.Charset$3.run
at java.nio.charset.Charset.availableCharsets
at ...CharsetAvailableCharsetsRaceTest...
最小复现代码:
- Code: Select all
import org.junit.Test;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.junit.Assert.assertFalse;
/**
* 最小复现:不经过任何 Spring/RestTemplate 代码,直接并发调 JDK 原生 {@code Charset.availableCharsets()}。
*
* <p>证明 {@link RestTemplateWriteAcceptCharsetRaceTest} 中竞态的根因不在 Spring,而在
* com.spire.doc 的 CharsetProvider(它的 jar 在项目 classpath 上,通过 SPI 被 JDK 自动加载):
* availableCharsets() 每次调用都会遍历所有 Provider,Spire 的 Provider 用非线程安全的静态
* ArrayList 惰性初始化,冷 JVM 并发首触即抛 AIOOBE/NPE/CME。本用例若复现,堆栈里将只有
* JDK + Spire 两类帧,没有任何 Spring 帧——Spring 只是触发路径。</p>
*
* <p>运行注意:依赖冷 JVM(本 JVM 内 Charset.availableCharsets() 未被触发过);若报"未复现",
* 全新 JVM 单独运行本类即可。</p>
*/
public class CharsetAvailableCharsetsRaceTest {
private static final int THREADS = 8;
private static final int ROUNDS_PER_THREAD = 3;
@Test
public void 并发调用availableCharsets_触发Spire竞态() throws Exception {
List<Throwable> errors = Collections.synchronizedList(new ArrayList<>());
ExecutorService pool = Executors.newFixedThreadPool(THREADS);
try {
CountDownLatch gate = new CountDownLatch(1);
List<Future<?>> futures = new ArrayList<>();
for (int t = 0; t < THREADS; t++) {
futures.add(pool.submit(() -> {
gate.await();
for (int i = 0; i < ROUNDS_PER_THREAD; i++) {
try {
Charset.availableCharsets();
} catch (Exception e) {
errors.add(e);
}
}
return null;
}));
}
gate.countDown();
for (Future<?> future : futures) {
future.get();
}
} finally {
pool.shutdownNow();
}
System.out.println("并发调 Charset.availableCharsets() 异常数:" + errors.size());
if (!errors.isEmpty()) {
errors.get(0).printStackTrace(System.out);
}
assertFalse("未复现竞态:本 JVM 的 Charset 可能已被提前初始化,请全新 JVM 单独运行本用例",
errors.isEmpty());
}
}
版本:
- Code: Select all
<dependency>
<groupId>e-iceblue</groupId>
<artifactId>spire.doc.free</artifactId>
<version>5.3.2</version>
</dependency>