1. 概述
本教程將探討導致 Java 拋出 UndeclaredThrowableException 異常的原因。
首先,我們將從一些理論知識入手。然後,我們將通過兩個實際案例,更好地理解該異常的本質。
2. 未聲明的異常 (UndeclaredThrowableException)
理論上講,Java 在嘗試拋出未聲明的檢查異常時,會拋出一個 UndeclaredThrowableException 的實例。 也就是説,我們在方法體中拋出該異常,但沒有在 throws 列表中聲明該檢查異常。
有人可能會認為這不可能發生,因為 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) {
// omitted
}如果代理本身拋出受檢異常,從調用者的角度來看,保存方法會拋出該受檢異常。 調用者可能對該代理一無所知,並將該異常歸咎於 保存方法。
在這種情況下,Java會將實際的受檢異常包裝在 未聲明異常中,並拋出 未聲明異常。 值得注意的是,未聲明異常本身是一個未檢查異常。
現在我們對理論有了足夠的瞭解,讓我們看看一些實際的例子。
3. Java 動態代理
作為我們第一個示例,讓我們為 java.util.List 接口創建一個運行時代理,並攔截其方法調用。首先,我們應該實現 <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/reflect/InvocationHandler.html">InvocationHandler</a> 接口,並將額外的邏輯放在其中:
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);
}
}此代理在代理的方法是<em>size</em>時,會拋出一個檢查式異常。否則,它將拋出一個未檢查式異常。
讓我們看看Java如何處理兩種情況。首先,我們將調用<em>List.size()</em>方法:
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)中拋出檢查異常(checked exception),且被建議的方法(advised methods)未聲明它們時,同樣會發生同樣的情況。 讓我們從一個註解開始:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThrowUndeclared {}現在我們將會指導所有帶有此註解的方法:
@Aspect
@Component
public class UndeclaredAspect {
@Around("@annotation(undeclared)")
public Object advise(ProceedingJoinPoint pjp, ThrowUndeclared undeclared) throws Throwable {
throw new SomeCheckedException("AOP Checked Exception");
}
}基本上,這項建議會要求所有帶有註釋的方法拋出被檢查異常,即使這些方法沒有聲明這樣的異常。現在,讓我們創建一個服務:
@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 將實際的異常封裝為原因,並拋出 UndeclaredThrowableException 異常。
5. 結論
在本教程中,我們瞭解到導致 Java 拋出 未聲明異常 (UndeclaredThrowableException) 異常的原因。