Optional i wyjątki

Optional i wyjątki
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Sprawa wygląda następująco, mam metodę która zwraca Optionala, którego wartość muszę użyć w kolejnej metodzie(makeRequest), która znowu może rzucić wyjątek. Próbowałem na wszelkie możliwe sposoby z map, ifPresent, i innymi, ale żaden sposób nie chcę działać. W końcu zrobiłem coś takiego

Kopiuj
 ActiveUserDTO getActiveUser(String sessionID) throws Exception {
        Optional<OAuth2AccessToken> accessToken = authorizationService.getAccessToken(UUID.fromString(sessionID));
        
        if(accessToken.isPresent()) {
            Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, accessToken.get());
            return new ActiveUserDTO(response.getBody());
        } else {
            throw new Exception("Something went wrong...");
        }
    }

W teorii robi to co chcę, jeżeli optional zwróci pustą wartość, poleci wyjątek który sobie jakoś obsłużę w fasadzie/controllerze, jeżeli uda się znaleźć wartość, strzelam do zewnętrznej usługi i znów, jeżeli uda się, zwracam wynik, jeżeli nie, rzucam wyjątek.

Za pewne jednak, da się to zrobić dużo ładniej niż optional.isPresent() - pytanie tylko jak, ktoś mógłby pomóc?

edytowany 3x, ostatnio: AngryProgrammer
NS
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:455
0

Nie widzę nic tragicznego w tym kodzie. Chyba, że zaraz rzucą się puryści, którym nic nie pasuje.
Jak używasz JDK >= 9, to użyj sobie metody ifPresentOrElse​(...)

AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Problem w tym, że ifPresentOrElse, itp, krzyczy że muszę wewnątrz niego obsłużyć wyjątki. A ja ewentualny wyjątek który poleci w makeRequest, chcę przekazać do klasy wyżej.

NS
  • Rejestracja:ponad 7 lat
  • Ostatnio:3 dni
  • Postów:455
0

No to złap wyjątek i rzuć sobie nowy RuntimeException może.

PI
  • Rejestracja:około 6 lat
  • Ostatnio:6 miesięcy
2

Optional ma jeszcze taką fajną metodę jak orElseThrow.

Kopiuj
OAuth2AccessToken accessToken = authorizationService.getAccessToken(UUID.fromString(sessionID))
                                                    .orElseThrow(() -> new Exception("Something went wrong..."));
Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, accessToken);
(...)
edytowany 2x, ostatnio: piotrusha
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
0

Jeszcze tylko pytanie na kiego grzyba ten wyjątek i co z nim robisz. Bo może lepiej po prostu zwrócić tego Optionala? Optional<ActiveUserDTO>


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0
jarekr000000 napisał(a):

Jeszcze tylko pytanie na kiego grzyba ten wyjątek i co z nim robisz. Bo może lepiej po prostu zwrócić tego Optionala? Optional<ActiveUserDTO>

Wyjątek może polecieć bo:
-komuś wygasł token, czyli ten Optional jest na starcie pusty
-coś poszło nie tak przy wywołaniu RESTa

Zwracając po prostu Optionala na dobrą sprawę nie wiem co się stało, a to istotne z punktu widzenia klienta aplikacji.

Wymyśliłem coś takiego

Kopiuj
ActiveUserDTO getActiveUser(String sessionID) throws RuntimeException {
     Optional<OAuth2AccessToken> accessToken = authorizationService.getAccessToken(UUID.fromString(sessionID));
     return accessToken.map(this::callMethod)
             .orElseThrow(() -> new RuntimeException("Token expired"));

 }

 private ActiveUserDTO callMethod(OAuth2AccessToken token) {
     try {
         Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, token);
         return new ActiveUserDTO(response.getBody());
     } catch (Exception e) {
         throw new RuntimeException("Error while invoking " + Constants.ALL_USERS_DATA_URL + e.getMessage());
     }
 }

Ale to w sumie rozwiązuje problem nieco prościej i czytelniej

Kopiuj
OAuth2AccessToken accessToken = authorizationService
                .getAccessToken(UUID.fromString(sessionID))
                .orElseThrow(() -> new RuntimeException("Token expired"));
DQ
  • Rejestracja:prawie 10 lat
  • Ostatnio:7 miesięcy
  • Postów:141
0

Szybkim rozwiązaniem mogłoby być nie rzucanie wyjątkiem, a użycie Try. W twoim przypadku mogłoby to wyglądać np. tak:

Kopiuj
 Try<ActiveUserDTO> getActiveUser(String sessionID) {
       authorizationService.getAccessToken(UUID.fromString(sessionID)).map(token -> {
          Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, token);
          return new ActiveUserDTO(response.getBody());
       }).toTry(() -> new Exception("Something went wrong"));

Nie mniej nie jest to wielce optymalne rozwiązanie, brnąc w Try'a to kod pi razy drzwi zapewne powinien mieć mniej więcej taką postać:

Kopiuj
Try<ActiveUserDTO> getActiveUser(String sessionID) {
    authorizationService
        .getAccessToken(UUID.fromString(sessionID))
        .toTry(() -> new Exception("could not get access token"))
        .flatMap(token -> authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, token))
        .map(response -> new ActiveUserDTO(response.getBody());
}

Nadal modelowanie potencjalnych błędów kuleje ale przynajmniej w jakiś tam sposób zabezpieczasz się przed błędami związanymi z wysyłaniem requestów.

Pytanie czego oczekujesz od tej metody. Jeśli nie interesuje Cię dlaczego coś się nie powiodło, a jedynie ostateczny wynik to wtedy Optional ma sens. Mówi bezpośrednio, że albo coś jest albo tego nie ma, bez żadnych szczegółów. Jeśli interesuje Cię przyczyna niepowodzenia to wtedy zdecydowanie bardziej nadaje się Try albo Either.

Twoja sygnatura metody (ActiveUserDTO getActiveUser(String sessionID) throws Exception ) zaprzecza temu po co zostały stworzone Optional, Try czy Either. Poinformuj potencjalnego użytkownika metody bezpośrednio o tym, że może coś pójść nie tak czy o tym, że może nie być wyniku.

AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Czyli zlym rozwiązaniem byłoby stworzenie dedykowanego wyjątku np. TokenExpiredException i rzucanie go w razie potrzeby z tej metody? Dodam ze to serwis zamknięty wewnątrz pakietu, dostępny tylko poprzez fasade.

Optional tutaj odpada z tego względu ze chce wiedzieć co konkretnie poszło nie tak. Zastanawia mnie jedynie przewaga Try badz Either nad zwykłym wyjątkiem pod ten konkretny przypadek.

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
4
  1. Zrobienie TokenExpiredException jest ok. W zamyśle twórców javy to powinno być zrobione jako checked exception (żeby klient wiedział, że ma obsłużyć). (ale małym programie możesz sobie zrobić dziedziczenie z RuntimeException jeśli skutki nieobsłużenia są niewielkie - przy runtime można łatwo zapomnieć).
  2. Skoro jest tylko jeden rodzaj problemu to Optional jest ok. Jak dostajesz empty to wiesz, że expired :-)
  3. Gdyby było więcej rodzajów problemów to możesz użyć Either<Problem, ActiveUserDTO>
    Gdzie Problem to np.:
Kopiuj
enum Problem {
 TokenExpired,
 BadToken,
 EvenWorseToken
}

(nie musi być enum, to może być dowolna klasa, np. rekord ze szczegółami błedu).
Przewagi Eithera nad CheckedException są takie:

  • referential transparency -> łatwiej testować - można np testować parametrycznie. Testowanie, że poleciał wyjątek wymaga ( dodatkowej gimnastyki, (małej ale jednak). Konsekwencje referential transparency są takie, że wyjątkiem/Either posługujesz sie jak zwykła wartością - możesz łatwiej przekazać do innych metod (np. jako listę). Natomiast klasyczne Exceptiony zawsze mają trochę osobny flow.
  • Rzucanie wyjątku jest troszkę bardziej kosztowne z punktu widzenia JVM (trzeba zapenić odtworzenie Stack Trace) - u Ciebie ten argument nie ma znaczenia, raczej nie będziesz miał 10k expired tokenów na sekundę,
  • klasa Left nie musi dziedziczyć z Exception/Throwable czyli np. łatwo zrobić enum, dużo mniej pitolenia jak masz dużo różnych błedów,
    (ale to pomniejszy problem, bo w wersji Checked Exceptiony opakowujesz dodatkowo tego enuma Exceptionem i masz prawie to samo... ).
  1. Try to taki ograniczony Either, gdzie problem (czyli Left) to Exception
    czyli:
    Try<T> =~= Either<Exception, T>
    (mniej więcnie równa się).
    Try ma raczej sens tam gdzie już są Exceptiony, bo lecą z istniejącego API i chcemy je opakować, bo lubimy referential transparency.

  2. Wszelkie błedy typu Panic - czyli np. co tam zły token, jak sieci nie ma - nadal najlepiej modelować jako RuntimeException, w założeniu, że w takiej sytuacji i tak obsługa błędu nie ma sensu. Można co najwyżej wywalić info na ekran, do logów lub zakończyć program.

  3. (uzupełnienie) Dla szeleńców, którzy kochają sterowanie (flow) przy pomocy Exceptionów, a płaczą nad wydajnością jest ratunek w postaci writableStackTrace (false)
    https://stackoverflow.com/questions/11434431/exception-without-stack-trace-in-java
    Nie polecam, ale jak ktoś bardzo lubi programować dyskunkcyjnie, to ma możliwość od Javy 7.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 13x, ostatnio: jarekr000000
CountZero
  • Rejestracja:prawie 8 lat
  • Ostatnio:12 miesięcy
  • Postów:262
0

@jarekr000000:
Mnie zawsze zastanawiało - jak zarządzać tranzakcjami (oczywiście w Springu), zrobić np. rollbacka, gdy zamiast wyjątków używa się Either lub Try? Czy trzeba w takim robić to ręcznie z pomocą np. TransactionTemplate? Jeśli tak, to wydaje się to być mega uciążliwe.

edytowany 1x, ostatnio: CountZero
Shalom
  • Rejestracja:ponad 21 lat
  • Ostatnio:około 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

Nie, nie będzie złym, tylko innym. Generalnie załóż że rzucenie wyjątku robisz kiedy handling jest obsługiwany gdzieś bardzo daleko. Np. robisz jakieś czary-mary w warstwie logiki i jak tam trafisz na to swoje TokenExpired to chcesz wylecieć w górę aż do Kontrolera i tam masz zapięty handler który taki wyjątek tłumaczy na odpowiedni kod błędu HTTP i msg. W takiej sytuacji zabawa w przepychanie Either przez X warstw w górę może zwyczajnie nie mieć sensu i niepotrzebnie komplikować kod.
Ale jeśli handling chcesz robić "blisko" np. jeden poziom wyżej, to rzucanie wyjątku, żeby go zaraz złapać, nie ma za bardzo sensu.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
TD
Taki wyjątek raczej powinien być checked więc w zasadzie wyjdzie na to samo.
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
0
CountZero napisał(a):

@jarekr000000:
Mnie zawsze zastanawiało - jak zarządzać tranzakcjami (oczywiście w Springu), zrobić np. rollbacka, gdy zamiast wyjątków używa się Either lub Try? Czy trzeba w takim robić to ręcznie z pomocą np. TransactionTemplate? Jeśli tak, to wydaje się to być mega uciążliwe.

Paradoks polega na tym, że jak używasz Either z @Transactional to się rollbackuje, nawet jak chcesz żeby się nie rollbackowało (bo obsłużyłeś błąd)...

TransactionTemplate uciążliwe?
Zamiast

Kopiuj
@Transactional
metoda (..) {
 kod
}

masz

Kopiuj
metoda (..) {
doInNewTransaction(() -> {
               kod
 }
}

Dochodzi linijka z klamrą. (Fakt, mamy TransactionTemplate pod tym doInNewTransaction schowany, coby jeszcze 2 linijki ocalić).


jeden i pół terabajta powinno wystarczyć każdemu
Aleksander Brzozowski
Zastanawiam się, czy nie lepiej zrobić doInNewTransaction, które przyjmuje Either/Try/Future, i zapiąć się tam na metodę onSuccess commit, onFailure rollback.
CountZero
  • Rejestracja:prawie 8 lat
  • Ostatnio:12 miesięcy
  • Postów:262
0

Paradoks polega na tym, że jak używasz Either z @Transactional to się rollbackuje, nawet jak chcesz żeby się nie rollbackowało (bo obsłużyłeś błąd).

Co masz na myśli?

Hmm, no dobra, może nie jakoś bardzo uciążliwe, ale za każdym razem wstrzykiwane serwisu i wrapowanie metod w lambdę jest wkurzające.

jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
0
CountZero napisał(a):

Co masz na myśli?

Transaction marked as rollback only już ileś razy się rozczarowałem.
Niby robię catch. Obsługuje wyjątek JPA (np. typu ConstraintViolationException).
Łapie go sobie i chcę zwrócić Optional lub Either...
A tu jeb - na wyjściu z metody Spring widzi, że transakcja jest oznaczona jako rollback only i rzuca za mnie exceptionem.
Bardzo dziękuję :-)

Typowe, ale wkurza, bo z tego co pamiętam to słabo jest udokumentowane i czasem całkiem niegroźne problemy JPA skutkują wymuszonym rollbackiem, i exceptionem rzuconym z d...


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
CountZero
Aaa, teraz rozumiem. Rzeczywiście dziwne zachowanie. Czemu tak to jest zrobione, jest jakiś powód inny niż "bo tak"?
jarekr000000
JPA trochę rzeczy trzyma w sesji (managed). jak poleci Exception z SQL to nie wiadomo czy to co mamy w sesji nie jest jakoś zepsute - i to nawet nie wiadomo dokładnie gdzie. Czyli ten wymuszony rollback ma nas powstrzymać przed zapisaniem czegoś przypadkowego do bazy danych. (przykład - podczas zapisu nowego obiektu obiekt dostał ID (z jakiegoś generatora), a potem poleciał Exception z JPA. I mamy w sesji dziwny obiekt, który ma ID != null/0, a jednocześnie nie jest w bazie zapisany. A to chyba najprostszy z dziwnych przypadków, który może się pojawić.
CountZero
Rzeczywiście... Brzmi jak nieco abstrakcyjny problem, zdarzył Ci się kiedyś? Ja bym pomyślał że w tej sytuacji którą podałeś JPA ogarnie że coś jest nie halo i przy kolejnej próbie zkomittowania lub wczytania encji wywali jakiś bliżej nieokreślony błąd.
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Mam mieszane uczucia co do Either... prawdopodobnie używam go w zły sposób, ale wydaje mi się to bardziej skomplikowane niż powinno... Ktoś spojrzy krytycznym okiem?

Kopiuj
 Either<ErrorDTO, ActiveUserDTO> getActiveUser(String sessionID) {
       return authorizationService
               .getAccessToken(UUID.fromString(sessionID))
               .map(this::callMethod)
               .orElse(Either.left(new TokenNotFoundDTO()));
   }

   private Either<RemoteMethodInvokeFailedDTO, ActiveUserDTO> callMethod(OAuth2AccessToken token) {
       try {
           Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, token);
           return Either.right(new ActiveUserDTO(response.getBody()));
       } catch (Exception e) {
           return Either.left(new RemoteMethodInvokeFailedDTO());
       }
   }

Dla porównania wersja z RuntimeException

Kopiuj
ActiveUserDTO getActiveUser(String sessionID) throws RuntimeException {
       OAuth2AccessToken accessToken = authorizationService
               .getAccessToken(UUID.fromString(sessionID))
               .orElseThrow(TokenNotFoundException::new);
       try {
           Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, accessToken);
           return new ActiveUserDTO(response.getBody());
       } catch (Exception e) {
           throw new RemoteMethodInvokeFailedException(e.getMessage());
       }
   }
edytowany 1x, ostatnio: AngryProgrammer
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 10 godzin
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4708
0

Problem masz tu:

Kopiuj
try {
            Response response = authorizationService.makeRequest(Verb.GET, Constants.ACTIVE_USER_URL, token);
            return Either.right(new ActiveUserDTO(response.getBody()));
        } catch (Exception e) {
            return Either.left(new RemoteMethodInvokeFailedDTO());
        }

Jakiego rodzaju błedy wyrzuca authorizationService.makeRequest i co możesz z nimi zrobić?
Czy jest możliwośc zmiany tej metody makeRequest?
catch (Exception e) to taki trochę rak (tylko czasami ma sens).

Edit: w zasadzie duży rak

Kopiuj
catch (Exception e) {
            throw new RemoteMethodInvokeFailedException(e.getMessage());
        }

Tego typu kod powoduje, że dowolny exception zostanie częściowo zgubiony (stack trace przepadnie i zostanie nam tylko message).
Jak już się tak bawimy exceptionami (łapiemy cokolwiek catch Exception) to warto oryginalny bład przechować.

Kopiuj
catch (Exception e) {
            throw new RruntimeException(e); // to nieco mniejszy rak
        }

jeden i pół terabajta powinno wystarczyć każdemu
edytowany 5x, ostatnio: jarekr000000
AP
throws InterruptedException, ExecutionException, IOException
jarekr000000
I w jaki sposób klient to obsługuje ? Wywołuje request jeszcze raz ?
AP
póki co zależy mi po prostu na wyciągnięciu tego z serwisu na controller i przygotowanie stosownej odpowiedzi, dlaczego coś się nie udało
jarekr000000
To albo niech authorizationService.makeRequest nie rzuca exceptionami tylko Eitherem. Albo niech makeRequest rzuca tylko RuntimeExceptionami.
Wibowit
  • Rejestracja:około 20 lat
  • Ostatnio:około 7 godzin
0
Shalom napisał(a):

Nie, nie będzie złym, tylko innym. Generalnie załóż że rzucenie wyjątku robisz kiedy handling jest obsługiwany gdzieś bardzo daleko. Np. robisz jakieś czary-mary w warstwie logiki i jak tam trafisz na to swoje TokenExpired to chcesz wylecieć w górę aż do Kontrolera i tam masz zapięty handler który taki wyjątek tłumaczy na odpowiedni kod błędu HTTP i msg. W takiej sytuacji zabawa w przepychanie Either przez X warstw w górę może zwyczajnie nie mieć sensu i niepotrzebnie komplikować kod.

Primo, TokenExpired to powinien być rzucany przed wejściem w logikę biznesową.
Secundo, sporo frameworków i aplikacji ma tendencję do tworzenia głębokich drzew wywołań funkcji. Często stosuje się tego typu schematy:

Kopiuj
A method1(params) {
// coś tam
method2(params)
// coś tam
}

B method2(params) {
// coś tam
method3(params)
// coś tam
}

C method3(params) {
// coś tam
method4(params)
// coś tam
}

D method4(params) {
// coś tam
method5(params)
// coś tam
}

Zamiast:

Kopiuj
A method1(params) {
// coś tam
B result2 = method2(params)
// coś tam
C result3 = method3(params)
// coś tam
D result4 = method4(params)
// coś tam
}

B method2(params) {
// coś tam
}

C method3(params) {
// coś tam
}

D method4(params) {
// coś tam
}

Nie zawsze da się tak spłaszczyć drzewo wywołań, ale próbować warto. Checked exceptions i przepychanie Eitherów przy rozwlekłej Javowej składni do tego powinny skłaniać.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 1x, ostatnio: Wibowit
AP
  • Rejestracja:około 6 lat
  • Ostatnio:3 miesiące
  • Postów:164
0

Właśnie druga rzecz która mnie zastanawia to organizacja tego wewnątrz całego pakietu, gdzie zwrócić Eithera, gdzie rzucić bądź obsłużyć wyjątek.
Na dnie mam "techniczny" AuthorizationService, który przechowuje informację o tokenach i strzela do zewnętrznego API.
Poziom wyżej są bardziej biznesowe serwisy jak np UserService którego kod tu pokazałem.
Dalej fasada - punkt wejścia do całego pakietu, inne paczki wewnątrz aplikacji komunikują się właśnie przez fasadę.
No i controller, RESTowa nakładka na fasade dla niektórych jej usług.

Staram się to napisać korzystając z możliwie jak najlepszych wzorców i praktyk, jednak im dalej w las tym ciemniej :)

edytowany 1x, ostatnio: AngryProgrammer

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.