Перейти к основному содержимому

Когда Java генерирует исключение UndeclaredThrowableException?

· 4 мин. чтения

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 поместит фактическое проверенное исключение в UndeclaredThrowableException и вместо этого сгенерирует UndeclaredThrowableException . Стоит отметить, что само UndeclaredThrowableException является непроверяемым исключением.

Теперь, когда мы достаточно знаем о теории, давайте посмотрим на несколько реальных примеров.

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);
}
}

Этот прокси выдает проверенное исключение, если прокси-метод имеет размер. В противном случае будет выдано непроверенное исключение.

Давайте посмотрим, как 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 , когда рекомендуемые методы их не объявляли. Начнем с аннотации:

@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 .

Как обычно, все примеры доступны на GitHub .