1、概览 {#1概览}
本文将带你了解 Java 抛出 UndeclaredThrowableException
异常的原因。
2、UndeclaredThrowableException {#2undeclaredthrowableexception}
从理论上讲,当我们尝试抛出一个未声明的受检异常时,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 动态代理 {#3java-动态代理}
第一个示例。为 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 {#4spring-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、总结 {#5总结}
本文介绍了 Java 抛出 UndeclaredThrowableException
异常的原因,以及出现该异常的常见场景。
Ref:https://www.baeldung.com/java-undeclaredthrowableexception