W końcu wrzuciliśmy wyjątek

27

Analizatory kodu statycznego, takie jak Fortify, „narzekają”, gdy wyjątek może zostać zgłoszony do finallybloku, mówiąc to Using a throw statement inside a finally block breaks the logical progression through the try-catch-finally. Zwykle się z tym zgadzam. Ale ostatnio natknąłem się na ten kod:

SomeFileWriter writer = null; 
try { 
     //init the writer
     //write into the file
} catch (...) {
     //exception handling
} finally {
     if (writer!= null) writer.close();  
}

Teraz, jeśli writernie można poprawnie zamknąć, writer.close()metoda zgłosi wyjątek. Należy zgłosić wyjątek, ponieważ (najprawdopodobniej) plik nie został zapisany po zapisaniu.

Mógłbym zadeklarować dodatkową zmienną, ustawić ją, jeśli wystąpił błąd podczas zamykania writeri zgłosić wyjątek po bloku w końcu. Ale ten kod działa dobrze i nie jestem pewien, czy go zmienić, czy nie.

Jakie są wady rzucenia wyjątku wewnątrz finallybloku?

superM
źródło
4
Jeśli jest to Java i możesz używać Java 7, sprawdź, czy bloki ARM mogą rozwiązać Twój problem.
Landei
@Landei, to rozwiązuje problem, ale niestety nie używamy Java 7.
superM
Powiedziałbym, że kod, który pokazałeś, nie brzmi „Używanie instrukcji throw w bloku wreszcie” i jako taki logiczny postęp jest w porządku.
Mike
@Mike, użyłem standardowego podsumowania, które pokazuje Fortify, ale bezpośrednio lub pośrednio istnieje w końcu wyjątek.
superM
Niestety blok Try-with-resources jest także wykrywany przez Fortify jako wyjątek w końcu wrzucony do środka ... jest zbyt sprytny, cholera .. wciąż nie jestem pewien, jak go pokonać, wydawało się, że powodem próby wypróbowania zasobów było zapewnienie zamknięcia zasoby w końcu, a teraz każde takie oświadczenie jest zgłaszane przez Fortify jako zagrożenie bezpieczeństwa.
mmona

Odpowiedzi:

18

Zasadniczo finallyistnieją klauzule zapewniające prawidłowe zwolnienie zasobu. Jeśli jednak w ostatnim bloku zostanie zgłoszony wyjątek, gwarancja zniknie. Co gorsza, jeśli główny blok kodu zgłasza wyjątek, wyjątek zgłoszony w finallybloku ukryje go. Wygląda na to, że błąd został wywołany przez wywołanie close, a nie z prawdziwego powodu.

Niektóre osoby stosują paskudny wzór zagnieżdżonych procedur obsługi wyjątków, połykając wszelkie wyjątki zgłoszone w finallybloku.

SomeFileWriter writer = null; 
try { 
     //init the writer
     //write into the file
} finally {
    if (writer!= null) {
        try {
            writer.close();
        } catch (...) {
        }
    }
}

W starszych wersjach Java możesz „uprościć” ten kod, owijając zasoby w klasy, które wykonują to „bezpieczne” czyszczenie. Mój dobry przyjaciel tworzy listę anonimowych typów, z których każdy zapewnia logikę oczyszczania swoich zasobów. Następnie jego kod po prostu zapętla listę i wywołuje metodę dispose w finallybloku.

Travis Parks
źródło
1
+1 Chociaż jestem wśród tych, którzy używają tego paskudnego kodu. Ale według mojego uzasadnienia powiem, że nie robię tego zawsze, tylko gdy wyjątek nie jest krytyczny.
superM
2
Ja też to robię. Czasami tworzenie dowolnego menedżera zasobów (anonimowego lub nie) szkodzi celowi kodu.
Travis Parks
+1 ode mnie też; w zasadzie powiedziałeś dokładnie, co zamierzam, ale lepiej. Warto zauważyć, że niektóre implementacje Stream w Javie nie mogą tak naprawdę zgłaszać wyjątków do close (), ale interfejs deklaruje to, ponieważ niektóre z nich tak robią. Tak więc w niektórych okolicznościach możesz dodawać blok catch, który tak naprawdę nigdy nie będzie potrzebny.
vaughandroid
1
Co jest tak złego w używaniu bloku try / catch w bloku wreszcie? Wolę to zrobić, niż nadużywanie całego mojego kodu przez konieczność zawinięcia wszystkich moich połączeń i strumieni itp. Tylko po to, aby można je było cicho zamknąć - co samo w sobie stanowi nowy problem polegający na tym, że nie wiem, czy zamknąć połączenie lub strumień ( lub cokolwiek innego) powoduje wyjątek - o czym właściwie możesz wiedzieć lub coś zrobić ...
zakaz geoinżynierii
10

To, co powiedział Travis Parks, jest prawdą, że wyjątki w finallybloku wykorzystują wszystkie zwracane wartości lub wyjątki od try...catchbloków.

Jeśli jednak używasz Java 7, problem można rozwiązać za pomocą bloku try-with-resources . Zgodnie z dokumentacją, o ile Twój zasób implementuje java.lang.AutoCloseable(większość pisarzy / czytelników robi to teraz), blok try-with-resources zamknie go dla ciebie. Dodatkową korzyścią jest to, że każdy wyjątek występujący podczas zamykania zostanie pominięty, umożliwiając pominięcie oryginalnej wartości zwracanej lub wyjątku.

Z

FileWriter writer = null;
try {
  writer = new FileWriter("myFile.txt");
  writer.write("hello");
} catch(...) {
  // return/throw new exception
} finally {
  writer.close(); // an exception would consume the catch block's return/exception
}

Do

try (FileWriter writer = new FileWriter("myFile.txt")) {
  writer.write("hello");
} catch(...) {
  // return/throw new exception, always gets returned even if writer fails to close
}

http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

clintonmonk
źródło
1
Czasami jest to pomocne. Ale w przypadku, gdy rzeczywiście muszę zgłosić wyjątek, gdy pliku nie można zamknąć, takie podejście ukrywa problemy.
superM
1
@superM W rzeczywistości try-with-resources nie ukrywa wyjątku close(). „każdy wyjątek, który wystąpi podczas zamykania, zostanie pominięty, umożliwiając pominięcie pierwotnej wartości zwracanej lub wyjątku” - to po prostu nieprawda . Wyjątek od close()metody zostanie pominięty tylko wtedy, gdy inny wyjątek zostanie zgłoszony z bloku try / catch. Zatem jeśli tryblok nie close()zgłosi żadnego wyjątku, wyjątek od metody zostanie zgłoszony (a zwracana wartość nie zostanie zwrócona). Można go nawet złapać z bieżącego catchbloku.
Ruslan Stelmachenko
0

Myślę, że jest to kwestia, którą należy rozwiązać indywidualnie dla każdego przypadku. W niektórych przypadkach to, co mówi analizator, jest poprawne, ponieważ kod, który posiadasz, nie jest tak świetny i wymaga ponownego przemyślenia. Ale mogą być inne przypadki, w których rzucanie lub nawet ponowne rzucanie może być najlepszą rzeczą. To nie jest coś, co możesz zlecić.

drekka
źródło
0

Będzie to bardziej konceptualna odpowiedź na pytanie, dlaczego te ostrzeżenia mogą istnieć nawet przy podejściu typu „spróbuj z zasobami”. Niestety nie jest to łatwe rozwiązanie, na które możesz liczyć.

Odzyskiwanie po błędzie nie może zakończyć się niepowodzeniem

finally modeluje przepływ kontrolny po transakcji, który jest wykonywany niezależnie od tego, czy transakcja się powiedzie, czy nie.

W przypadku fail, finallyprzechwytuje logiczne, że jest wykonywany w środku odzyskania z błędu, zanim został on w pełni sprawny (zanim osiągniemy nasz catchcel).

Wyobraźmy sobie, że problemy koncepcyjne prezentuje do wystąpienia błędu w środku odzyskania z błędu.

Wyobraź sobie serwer bazy danych, w którym próbujemy dokonać transakcji, a transakcja kończy się niepowodzeniem (powiedzmy, że serwerowi zabrakło pamięci w środku). Teraz serwer chce wycofać transakcję do punktu, tak jakby nic się nie stało. Wyobraź sobie jednak, że napotyka kolejny błąd w procesie wycofywania. Teraz mamy do czynienia z transakcją w połowie zatwierdzoną w bazie danych - atomowość i niepodzielny charakter transakcji jest teraz zepsuty, a integralność bazy danych zostanie teraz zagrożona.

Ten problem koncepcyjny występuje w każdym języku, który zajmuje się błędami, niezależnie od tego, czy jest to C z ręczną propagacją kodu błędu, C ++ z wyjątkami i destruktorami, czy Java z wyjątkami i finally.

finally nie może zawieść w językach, które zapewniają to w ten sam sposób, w jaki niszczyciele nie mogą zawieść w C ++ podczas napotkania wyjątków.

Jedynym sposobem na uniknięcie tego koncepcyjnego i trudnego problemu jest upewnienie się, że proces wycofywania transakcji i zwalniania zasobów w środku nie może napotkać rekurencyjnego wyjątku / błędu.

Zatem jedynym bezpiecznym projektem jest tutaj projekt, w którym writer.close()nie może zawieść. Zazwyczaj istnieją sposoby, aby uniknąć scenariuszy, w których takie rzeczy mogą zawieść w trakcie odzyskiwania, co uniemożliwia.

Jest to niestety jedyny sposób - odzyskiwanie po błędzie nie może zakończyć się niepowodzeniem. Najprostszym sposobem, aby to zapewnić, jest uniemożliwienie tego rodzaju „uwolnienia zasobów” i „odwrotnych efektów ubocznych”. To nie jest łatwe - prawidłowe odzyskiwanie błędów jest trudne, a także niestety trudne do przetestowania. Ale sposobem na osiągnięcie tego jest upewnienie się, że wszelkie funkcje, które „niszczą”, „zamykają”, „zamykają”, „wycofują” itp., Nie mogą napotkać zewnętrznego błędu w procesie, ponieważ takie funkcje często będą musiały zostać wywołanym w trakcie odzyskiwania po istniejącym błędzie.

Przykład: rejestrowanie

Powiedzmy, że chcesz zalogować rzeczy w finallybloku. Jest to często poważny problem, chyba że rejestracja nie powiedzie się . Logowanie prawie na pewno może się nie powieść, ponieważ może chcieć dołączyć więcej danych do pliku, a to może łatwo znaleźć wiele przyczyn niepowodzenia.

Rozwiązaniem jest tutaj, aby żadna funkcja rejestrowania używana w finallyblokach nie mogła przekazać obiektu wywołującego (może się nie powieść, ale nie wyrzuci). Jak możemy to zrobić? Jeśli twój język pozwala na rzucanie w kontekście pod warunkiem, że istnieje zagnieżdżony blok try / catch, byłby to jeden ze sposobów uniknięcia rzucania dzwoniącemu przez połykanie wyjątków i zamienianie ich w kody błędów, np. Może rejestrowanie może odbywać się w osobnym proces lub wątek, które mogą zawieść osobno i poza istniejącym stosem odzyskiwania po awarii, rozwijają się. Tak długo, jak możesz komunikować się z tym procesem bez możliwości wystąpienia błędu, byłoby to również bezpieczne, ponieważ problem bezpieczeństwa występuje tylko w tym scenariuszu, jeśli rekurencyjnie rzucamy z tego samego wątku.

W takim przypadku możemy uniknąć awarii rejestrowania, pod warunkiem, że nie rzuca się, ponieważ nie można się zalogować i nic nie robić, to nie koniec świata (nie wycieknie żadnych zasobów lub nie przywróci efektów ubocznych, np.).

W każdym razie jestem pewien, że możesz już zacząć wyobrażać sobie, jak niesamowicie trudno jest naprawdę uczynić oprogramowanie bezpiecznym dla wyjątków. Być może nie będzie konieczne uzyskanie tego w jak największym stopniu we wszystkich programach oprócz najbardziej krytycznych dla misji. Warto jednak zwrócić uwagę na to, jak naprawdę osiągnąć bezpieczeństwo wyjątków, ponieważ nawet autorzy bibliotek o bardzo ogólnym przeznaczeniu często grzebią tutaj i niszczą całe bezpieczeństwo wyjątków aplikacji korzystającej z biblioteki.

SomeFileWriter

Jeśli SomeFileWritermożna wrzucić do środka close, powiedziałbym, że jest to generalnie niezgodne z obsługą wyjątków, chyba że nigdy nie spróbujesz go zamknąć w kontekście wymagającym odzyskania z istniejącego wyjątku. Jeśli kod jest poza twoją kontrolą, możemy być SOL, ale warto powiadomić autorów tego rażącego problemu dotyczącego bezpieczeństwa wyjątków. Jeśli jest to twoja kontrola, moim głównym zaleceniem jest upewnienie się, że zamknięcie go nie może zakończyć się niepowodzeniem za pomocą wszelkich niezbędnych środków.

Wyobraź sobie, że system operacyjny może nie zamknąć pliku. Teraz każdy program, który próbuje zamknąć plik podczas zamykania, nie zostanie zamknięty . Co powinniśmy teraz zrobić, po prostu utrzymuj aplikację otwartą i zawieszoną (prawdopodobnie nie), po prostu wyciek zasobu pliku i zignoruj ​​problem (może w porządku, jeśli nie jest to takie krytyczne)? Najbezpieczniejszy projekt: spraw, aby zamknięcie pliku było niemożliwe.


źródło