Después de cambiar de Java 11 a Java 17 (OpenJDK instalado desde el repositorio de Ubuntu 20.04), el siguiente código no funciona:
import java.util.Objects; import java.util.concurrent.CompletableFuture; import org.apache.commons.lang.builder.ReflectionToStringBuilder; import org.apache.commons.lang3.exception.ExceptionUtils; public class TestClass { public static void main(String[] args) { CompletableFuture<String> streamFuture = CompletableFuture.supplyAsync(() -> { throw MyException.wrapIfNeeded(new Exception("MyException")); }); String result = null; try { result = streamFuture.get(); } catch (Exception e) { System.out.println("Exception: " + ExceptionUtils.getMessage(e)); } System.out.println("Result: " + Objects.toString(result)); } static class MyException extends RuntimeException { private static final long serialVersionUID = 3349188601484197015L; public MyException(Throwable cause) { super(cause == null ? null : cause.getMessage(), cause); } public static MyException wrapIfNeeded(Throwable e) { return e instanceof MyException ? (MyException) e : new MyException(e); } @Override public String toString() { return ReflectionToStringBuilder.toString(this); } } } Hay un problema en streamFuture.get() : se cuelga infinitamente. Profundicé más y descubrí que en java.util.concurrent.ForkJoinPool hay un método unmanagedBlock(ManagedBlocker blocker) que parece
/** ManagedBlock for external threads */ private static void unmanagedBlock(ManagedBlocker blocker) throws InterruptedException { if (blocker == null) throw new NullPointerException(); do {} while (!blocker.isReleasable() && !blocker.block()); }y el programa se cuelga infinitamente en el bucle do-while.
EDITAR: descubrí que el problema se debe al método toString() agregado a mi clase de excepción personalizada. Por alguna razón, comenzó a ser un problema después de Java 11
ReflectionToStringBuilder.toString usa la API Java encapsulada. Podemos probar algo como MyException.wrapIfNeeded(new NullPointerException("")).toString() Y veremos
Excepción en el subproceso "principal" java.lang.reflect.InaccessibleObjectException: no se puede hacer que el campo sea estático final largo java.lang.RuntimeException.serialVersionUID accesible: el módulo java.base no "abre java.lang" en el módulo sin nombre @ b4c966a
En el método CompletableFuture.AsyncSupply#run
static final class AsyncSupply<T> extends ForkJoinTask<Void> implements Runnable, AsynchronousCompletionTask { ... public void run() { CompletableFuture<T> d; Supplier<? extends T> f; if ((d = dep) != null && (f = fn) != null) { dep = null; fn = null; if (d.result == null) { try { d.completeValue(f.get()); } catch (Throwable ex) { d.completeThrowable(ex); } } d.postComplete(); } } Dado que se lanza MyException , d.completeThrowable(ex); se llama, dentro de este método, envolverá la excepción en CompletionException , que llamará al constructor de Throwable con MyException como cause .
public Throwable(Throwable cause) { fillInStackTrace(); detailMessage = (cause==null ? null : cause.toString()); this.cause = cause; } Cuando se llama a cause.toString() , arrojará la excepción mencionada al principio, por lo que d.completeThrowable(ex); no se ejecuta completamente y, por lo tanto, el programa se colgará para siempre.
Descubrí que el problema se debe al método toString() agregado a mi clase de excepción personalizada. Por alguna razón (o para ser más precisos: ReflectionToStringBuilder.toString(this); ) comenzó a ser un problema después de Java 11.