Este ano eu tive que fazer a migração de uma aplicação do JDK8 para o JDK11 – sim, o mundo corporativo, por mais ágil que seja, não acompanha com tanta avidez as novas releases de tecnologias e ferramentas que aparecem em intervalos cada vez menores.
Na última migração que eu fiz foram inúmeros erros. Mas o último deles me deixou travado por alguns dias, foi este infame SAAJMetaFactoryImpl not found.
Caused by: javax.xml.soap.SOAPException: Unable to create SAAJ meta-factory: Provider com.sun.xml.internal.messaging.saaj.soap.SAAJMetaFactoryImpl not found
Nem o chatGPT deu conta. A única coisa que ele sugeriu foi adicionar N dependências no pom.xml e nenhuma delas resolvia o problema. Stackoverflow não foi diferente, muitas sugestões para adicionar dependências, mas nenhuma delas funcionava.
O ponto de partida para encontrar a solução veio do próprio código. O erro sempre vinha do mesmo lugar: uma chamada SOAP em uma classe util, mas essa chamada funcionava em alguns casos. Ao analisar as chamadas que não funcionavam descobri algo em comum, todas eram feitas em contextos assíncronos por meio de Parallel e assim cheguei a um tópico no GitHub sobre essa questão.
public class ParallelServicesUtil {
private static ExecutorService executor = Executors.newWorkStealingPool(60);
public static Future<Object> callAsync(Callable<Object> service) throws MyException {
return executor.submit(service);
}
public static List<Object> execute( List<Callable<Object>> callables) throws MyException {
try {
List<Future<Object>> futures;
futures = executor.invokeAll(callables);
List<Object> result = new ArrayList<>();
for(Future<Object> future : futures){
result.add(future.get());
}
return result;
} catch (Exception e) {
log.error("Ocorreu um erro na execução paralela", e);
throw new MyException(e);
}
}
@SafeVarargs
public static List<Object> execute(Callable<Object>...callables) throws MyException {
return execute(Arrays.asList(callables));
}
}
Em aplicações Java empacotadas, como no SpringBoot, o class loader que carrega as classes é muitas vezes um class loader customizado, como:
TomcatEmbeddedWebappClassLoader
LaunchedURLClassLoader
(usado pelo Spring Boot)
O problema é que threads criadas fora do controle da aplicação (como pelo ForkJoinPool.commonPool
) não herdam esse class loader, e por isso falham ao tentar carregar algumas classes.
O erro é, na verdade, um problema de class loader. Quando se usa parallelStream()
ou stream().parallel()
, por padrão, o código executa tarefas em paralelo usando o ForkJoinPool.commonPool()
. Esse pool usa threads que não herdam o class loader correto (o chamado Thread Context Class Loader – TCCL) em aplicações empacotadas (JARs applications).
Após analisar a discussão no GitHub pensei em adicionar o class loader no contexto da thread paralela de forma explicita, como uma propagação de contexto. E deu certo! Agora as threads paralelas são criadas a partir de um ExecutorService
que herda corretamente o TCCL.
public class ParallelServicesUtil {
private static final ExecutorService executor = Executors.newWorkStealingPool(60);
public static CompletableFuture<Object> callAsync(Callable<Object> service) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
return CompletableFuture.supplyAsync(() -> {
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
try {
currentThread.setContextClassLoader(contextClassLoader);
return service.call();
} catch (Exception e) {
throw new CompletionException(e);
} finally {
currentThread.setContextClassLoader(originalClassLoader);
}
}, executor);
}
public static List<Object> execute(List<Callable<Object>> callables) throws MyException {
try {
List<CompletableFuture<Object>> futures = callables.stream()
.map(ParallelServicesUtil::callAsync)
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
List<Object> result = new ArrayList<>();
for (CompletableFuture<Object> future : futures) {
result.add(future.get());
}
return result;
} catch (Exception e) {
log.error("Ocorreu um erro na execução paralela", e);
throw new MyException(e);
}
}
@SafeVarargs
public static List<Object> execute(Callable<Object>... callables) throws MyException {
return execute(Arrays.asList(callables));
}
}
Esses são os pontos principais do novo código:
- Explicitar o class loader correto antes da execução da thread paralela: Thread.currentThread().setContextClassLoader(contextClassLoader).
- Substituir o
executor.submit(...)
pelo CompletableFuture, pois o primeiro não propaga o class loader correto.