Why commenting out line to simulate failure don't throw compile error
@Slf4j
@Service
@RequiredArgsConstructor
public class NewAssetEventListener implements DomainEventListener<NewAssetEvent> {
private final AssetsRepository assetsRepository;
private final CloudStorageService cloudStorageService;
@ApplicationModuleListener
@Override
public void handleEvent(NewAssetEvent event) {
try {
log.info("Received AssetCreated event: {}. Starting cloud upload...", event);
if(1+1==2) {
throw new Exception("Simulate failure");
}
Asset asset = assetsRepository.getAsset(event.assetId());
asset = asset.onAssetUploadedToCloud();
cloudStorageService.upload(asset);
assetsRepository.save(asset);
} catch (InvalidAssetStatusException e) {
log.error("Asset aggregate already uploaded to the cloud{}", event);
} catch (Exception e) {
log.error("Unable to persis Asset aggregate after event {}", event);
throw e;
}
}
}
Jasne, wyjaśnijmy to krok po kroku. To świetne pytanie, które dotyka samego serca obsługi wyjątków w Javie.
Krótka odpowiedź
Dostajesz ten błąd, ponieważ Twoja metoda handleEvent nie deklaruje, że może "wyrzucić" na zewnątrz wyjątek typu Exception. Kiedy w bloku catch ponownie rzucasz ten wyjątek za pomocą throw e;, kompilator Javy protestuje, mówiąc: "Hej, chcesz rzucić wyjątek, o którym nie uprzedziłeś w sygnaturze metody! To niedozwolone dla tego typu wyjątku."
Dokładne wyjaśnienie: Wyjątki checked vs unchecked
W Javie wyjątki dzielą się na dwie główne kategorie, co jest kluczowe do zrozumienia Twojego problemu:
- Wyjątki
checked(sprawdzane)
- To są wyjątki, które dziedziczą po klasie
Exception, ale nie poRuntimeException. Przykłady:IOException,SQLException. - Kompilator Javy zmusza Cię do ich obsługi. Masz dwie opcje:
- Złapać je w bloku
try...catch. - Zadeklarować, że Twoja metoda może je rzucić, dodając
throws NazwaWyjatkudo sygnatury metody. - Są one przeznaczone do sytuacji, na które programista ma wpływ i które mogą wystąpić nawet w poprawnie działającym programie (np. brak pliku, problem z połączeniem do bazy danych).
- Wyjątki
unchecked(niesprawdzane)
- To są wyjątki, które dziedziczą po klasie
RuntimeException(orazError). Przykłady:NullPointerException,IllegalArgumentException,ArrayIndexOutOfBoundsException. - Kompilator nie wymaga od Ciebie ich obsługi. Możesz je łapać, ale nie musisz.
- Zazwyczaj sygnalizują błędy programistyczne (np. odwołanie się do obiektu
null, dzielenie przez zero) lub krytyczne błędy środowiska.
Analiza Twojego kodu krok po kroku
Zobaczmy, co się dzieje, gdy odkomentujesz linię:
throw new Exception("Simulate failure");
- Świadomie rzucasz wyjątek ogólnego typu
java.lang.Exception. - To jest wyjątek typu
checked!
catch (Exception e) { ... }
- Twój kod poprawnie łapie ten wyjątek. Do tego momentu wszystko jest w porządku. Blok
try...catchzadziałał.
throw e;
- Wewnątrz bloku
catchponownie rzucasz ten sam złapany wyjątek. - I tu jest sedno problemu. Rzucasz wyjątek
checked(Exception) z wnętrza metodyhandleEvent.
public void handleEvent(NewAssetEvent event)
- Spójrz na sygnaturę (nagłówek) Twojej metody. Ona nie deklaruje, że może rzucić jakikolwiek wyjątek typu
checked. Wygląda tak, jakby obiecywała, że "połknie" wszystkie problemy i nic nie wydostanie się na zewnątrz.
Kompilator Javy widzi to tak:
"Metoda
handleEventnie ma w swojej sygnaturze klauzulithrows Exception, a mimo to próbujesz z niej rzucić wyjątek typuchecked. To jest niezgodne z zasadami języka. Musisz albo obsłużyć ten wyjątek na miejscu (ale Ty go właśnie rzucasz!), albo zadeklarować, że ta metoda może go rzucić."
Dlatego właśnie dostajesz błąd: Unhandled exception: java.lang.Exception (Nieobsłużony wyjątek).
Isn't then catch(Exception e) dead code? why compile don't complain and allow to keep it with throw when it knows it will never will be executed
To jest genialne pytanie! Trafiasz w samo sedno tego, jak działa kompilator Javy i czym różni się "martwy kod" od kodu, który jest po prostu "potencjalnie wykonywalny".
Odpowiedź brzmi: Nie, ten kod nie jest martwy. Jest on jak najbardziej "żywy" i kompilator traktuje go bardzo poważnie.
Wyjaśnijmy, dlaczego tak jest i co dokładnie sprawdza kompilator.
Analogia: Gaśnica w kuchni
Pomyśl o bloku try...catch jak o gaśnicy zamontowanej w kuchni.
try { ... }: To jest Twoja kuchnia, gdzie gotujesz. Zazwyczaj wszystko idzie dobrze.catch (Exception e) { ... }: To jest Twoja gaśnica. Jest na ścianie na wypadek, gdyby coś się zapaliło (gdyby został rzucony wyjątek).
Czy to, że masz gaśnicę, oznacza, że na 100% będzie pożar? Nie. Czy to, że dzisiaj nie było pożaru, oznacza, że gaśnica jest "martwa" i niepotrzebna? Też nie!
Masz ją, ponieważ istnieje ryzyko pożaru. Kompilator myśli dokładnie w ten sam sposób.
Co kompilator wie, a czego nie wie?
Kompilator jest mądry, ale nie jest wszechwiedzący. Zobaczmy, co analizuje w Twoim bloku try:
try {
log.info("Received AssetCreated event: {}. Starting cloud upload...", event);
// Ta linia jest zakomentowana, więc kompilator ją ignoruje
// if(1+1==2) { throw new Exception("Simulate failure"); }
Asset asset = assetsRepository.getAsset(event.assetId()); // <-- Punkt 1
asset = asset.onAssetUploadedToCloud(); // <-- Punkt 2
cloudStorageService.upload(asset); // <-- Punkt 3
assetsRepository.save(asset); // <-- Punkt 4
}
Kompilator patrzy na ten kod i zadaje sobie pytanie: "Czy jakakolwiek z tych operacji może potencjalnie rzucić wyjątek typu Exception (lub jego podklasę, np. RuntimeException)?".
assetsRepository.getAsset(...): To jest wywołanie metody. Kompilator nie analizuje jej wewnętrznej logiki, ale wie, że każda metoda w Javie może rzucićRuntimeException(np.NullPointerException, jeśliassetsRepositoryjestnull). Co więcej, ta metoda może być zadeklarowana jakothrows JakiśWyjątekChecked.asset.onAssetUploadedToCloud(): To samo. Może rzucićRuntimeException.cloudStorageService.upload(asset): To samo. Ta metoda prawie na pewno może rzucić np.IOException(który jest wyjątkiemchecked), jeśli wystąpi problem z siecią.assetsRepository.save(asset): To samo. Może rzucić wyjątek związany z bazą danych, np.DataAccessExceptionw Springu.
Wniosek kompilatora: Ponieważ blok try zawiera wywołania metod, które mogą rzucić wyjątki łapane przez catch (Exception e), ten blok catch nie jest martwy. Jest potencjalnie osiągalny. Dlatego nie ma błędu kompilacji.
Kiedy kod w catch byłby martwy (Unreachable Code)?
Kompilator zgłosiłby błąd "unreachable catch block" (nieosiągalny blok catch) tylko wtedy, gdyby mógł udowodnić, że w bloku try absolutnie nic nie jest w stanie rzucić wyjątku danego typu.
Zobacz na ten przykład:
try {
int a = 5;
int b = 10;
int c = a + b;
System.out.println("Wynik to: " + c);
} catch (java.io.IOException e) { // <-- BŁĄD KOMPILACJI!
// Ten kod jest martwy (nieosiągalny)
System.out.println("To się nigdy nie wykona");
}
W powyższym przykładzie kompilator widzi, że:
- W bloku
trysą tylko proste operacje arytmetyczne i wypisanie na konsolę. - Żadna z tych operacji nie jest w stanie rzucić wyjątku
java.io.IOException(który jestchecked). - W związku z tym blok
catchdlaIOExceptionjest bezużyteczny i nigdy się nie wykona. Kompilator zgłasza błąd, żeby Cię o tym poinformować i zmusić do usunięcia "martwego kodu".
Ważna uwaga: Gdybyś łapał catch (Exception e) lub catch (RuntimeException e), błędu by nie było, bo kompilator zakłada, że teoretycznie coś może pójść nie tak (choć w tym prostym przypadku jest to mało prawdopodobne). Ale w przypadku konkretnych wyjątków checked jest bardziej rygorystyczny.
Podsumowanie
- Twój kod nie jest martwy: Blok
catch (Exception e)jest dozwolony, ponieważ kod w blokutrywywołuje metody, które potencjalnie mogą rzucić wyjątki (zarównochecked, jak iunchecked), które są łapane przezException. - Kompilator nie jest wszechwiedzący: Nie analizuje, czy faktycznie w danej chwili poleci wyjątek, a jedynie czy istnieje taka możliwość zgodnie z regułami języka.
- Błąd pojawia się dopiero później: Problem, który opisałeś w pierwszym pytaniu, nie wynika z tego, czy
catchjest osiągalny, ale z tego, co robisz w jego wnętrzu. Kiedy ponownie rzucaszthrow e;, próbujesz "wypchnąć" problem na zewnątrz, a do tego potrzebujesz już formalnej zgody w postacithrows Exceptionw sygnaturze metody.
Twój blok catch jest więc poprawnym zabezpieczeniem na wypadek awarii, a nie "martwym kodem".