Java 中的 UndeclaredThrowableException 异常

1、概览

本文将带你了解 Java 抛出 UndeclaredThrowableException 异常的原因。

2、UndeclaredThrowableException

从理论上讲,当我们尝试抛出一个未声明的受检异常时,Java 会抛出一个 UndeclaredThrowableException 异常。也就是说,我们没有在 throws 子句中声明受检异常,但却在方法体中抛出了该异常。

受检异常 - 指的是必须要调用者用 try/catch 语句处理或者是再次 throws 出去的异常(即非 RuntimeException 子类)。

有人可能会说这是不可能的,因为 Java 编译器会通过编译错误来防止这种情况发生。

例如,如果我们尝试编译:

public void undeclared() {
    throw new IOException();
}

Java 编译器提示的失败信息如下:

java: unreported exception java.io.IOException; must be caught or declared to be thrown

尽管在编译时可能不会抛出未声明的受检异常,但在运行时仍有可能发生。

例如,一个运行时代理拦截一个不抛出任何异常的方法:

public void save(Object data) {
    // 省略
}

如果代理本身抛出了受检异常,从调用者的角度来看,save 方法也会抛出受检异常。调用者可能对该代理一无所知,因此会将该异常归咎于 save 方法。

在这种情况下,Java 会将实际已检查异常封装在 UndeclaredThrowableException 中,然后抛出 UndeclaredThrowableException。而 UndeclaredThrowableException 本身就是一个 非受检异常(RuntimeException

了解了理论后,来看看几个现实世界中的例子。

3、Java 动态代理

第一个示例。为 java.util.List 接口创建一个运行时代理,并拦截其方法调用。首先,实现 InvocationHandler 接口,并在其中加入额外的逻辑:

public class ExceptionalInvocationHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("size".equals(method.getName())) {
            throw new SomeCheckedException("Always fails");
        }
            
        throw new RuntimeException();
    }
}

public class SomeCheckedException extends Exception {

    public SomeCheckedException(String message) {
        super(message);
    }
}

如果被代理的方法有 size,该代理会抛出 受检异常。否则,它会抛出一个 非受检异常

来看看 Java 是如何处理这两种情况的。首先,调用 List.size() 方法:

ClassLoader classLoader = getClass().getClassLoader();
InvocationHandler invocationHandler = new ExceptionalInvocationHandler();
List<String> proxy = (List<String>) Proxy.newProxyInstance(classLoader, 
  new Class[] { List.class }, invocationHandler);

assertThatThrownBy(proxy::size)
  .isInstanceOf(UndeclaredThrowableException.class)
  .hasCauseInstanceOf(SomeCheckedException.class);

如上所示,为 List 接口创建了一个代理,并在其上调用 size 方法。而代理会拦截调用并抛出一个受检异常。然后,Java 将该异常封装在 UndeclaredThrowableException 实例中。之所以会出现这种情况,是因为在方法声明中没有声明就抛出了受检异常。

如果调用 List 接口上的任何其他方法:

assertThatThrownBy(proxy::isEmpty).isInstanceOf(RuntimeException.class);

由于代理会抛出一个 非受检异常,Java 会让异常继续传播。

4、Spring Aspect

当在 Spring Aspect 中抛出受检异常,而 Advice 方法却没有声明它们时,也会发生同样的情况。

首先,定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThrowUndeclared {}

然后,在所有 Advice 方法上使用此注解:

@Aspect
@Component
public class UndeclaredAspect {

    @Around("@annotation(undeclared)")
    public Object advise(ProceedingJoinPoint pjp, ThrowUndeclared undeclared) throws Throwable {
        throw new SomeCheckedException("AOP Checked Exception");
    }
}

这个 AOP,会让所有注解了 @ThrowUndeclared 的 Advice 方法抛出一个受检异常,而被代理的方法本身并未声明这个异常。

@Service
public class UndeclaredService {

    @ThrowUndeclared
    public void doSomething() {}
}

如果调用注解方法,Java 将抛出一个 UndeclaredThrowableException 异常:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = UndeclaredApplication.class)
public class UndeclaredThrowableExceptionIntegrationTest {

    @Autowired private UndeclaredService service;

    @Test
    public void givenAnAspect_whenCallingAdvisedMethod_thenShouldWrapTheException() {
        assertThatThrownBy(service::doSomething)
          .isInstanceOf(UndeclaredThrowableException.class)
          .hasCauseInstanceOf(SomeCheckedException.class);
    }
}

如上所示,Java 将实际异常封装为 cause,并抛出 UndeclaredThrowableException 异常。

5、总结

本文介绍了 Java 抛出 UndeclaredThrowableException 异常的原因,以及出现该异常的常见场景。


Ref:https://www.baeldung.com/java-undeclaredthrowableexception