Po raz pierwszy w życiu znalazłem się w sytuacji, w której piszę interfejs API języka Java, który będzie dostępny na zasadach open source. Mam nadzieję, że zostaną uwzględnione w wielu innych projektach.
Do logowania ja (i ludzie, z którymi pracuję) zawsze korzystałem z JUL (java.util.logging) i nigdy nie miałem z tym żadnych problemów. Jednak teraz muszę bardziej szczegółowo zrozumieć, co powinienem zrobić, aby opracować interfejs API. Zrobiłem trochę badań na ten temat, a dzięki posiadanym informacjom jestem coraz bardziej zdezorientowany. Stąd ten post.
Ponieważ pochodzę z JUL, jestem na to stronniczy. Moja wiedza na temat reszty nie jest tak duża.
Z przeprowadzonych przeze mnie badań wymyśliłem następujące powody, dla których ludzie nie lubią JUL:
„Zacząłem programować w Javie na długo przed wydaniem Sun przez JUL i łatwiej było mi kontynuować logowanie do frameworka-X niż nauczyć się czegoś nowego” . Hmm Nie żartuję, tak mówią ludzie. Z tym argumentem wszyscy moglibyśmy robić COBOL. (jednak z pewnością mogę odnieść się do tego, że jestem leniwym facetem)
„Nie podoba mi się nazwy poziomów rejestrowania w JUL” . Ok, poważnie, to po prostu za mało, by wprowadzić nową zależność.
„Nie podoba mi się standardowy format wyjścia z JUL” . Hmm To tylko konfiguracja. Nie musisz nawet nic robić kodowo. (to prawda, w dawnych czasach być może trzeba było stworzyć własną klasę Formatter, aby wszystko było dobrze).
„Korzystam z innych bibliotek, które również używają rejestrowania-framework-X, więc pomyślałem, że łatwiej jest po prostu z niego korzystać” . To jest okrągły argument, prawda? Dlaczego „wszyscy” używają rejestrowania-framework-X, a nie JUL?
„Wszyscy inni używają rejestrowania-framework-X” . To dla mnie tylko szczególny przypadek powyższego. Większość nie zawsze ma rację.
Tak więc prawdziwe wielkie pytanie brzmi: dlaczego nie JUL? . Czego mi brakowało? Raison d'être dla rejestrowania fasad (SLF4J, JCL) polega na tym, że wiele implementacji rejestrowania istniało historycznie, a powód tego naprawdę sięga czasów sprzed JUL. Gdyby JUL był idealny, to nie byłoby rejestrowania fasad, czy co? Sprawienie, by sprawy były bardziej mylące, JUL jest w pewnym stopniu samą fasadą, umożliwiającą zamianę Handlerów, Formatterów, a nawet LogManagera.
Czy zamiast pytać o wiele sposobów robienia tego samego (rejestrowanie), nie powinniśmy pytać, dlaczego były one konieczne? (i sprawdź, czy te powody nadal istnieją)
Ok, moje dotychczasowe badania doprowadziły do kilku rzeczy, które widzę, mogą być prawdziwymi problemami z JUL:
Wydajność . Niektórzy twierdzą, że wydajność w SLF4J jest lepsza niż reszta. Wydaje mi się, że jest to przypadek przedwczesnej optymalizacji. Jeśli musisz rejestrować setki megabajtów na sekundę, to nie jestem pewien, czy idziesz właściwą drogą. JUL ewoluował również, a testy przeprowadzone na Javie 1.4 mogą już nie być prawdziwe. Możesz przeczytać o tym tutaj, a ta poprawka wprowadziła ją do Java 7. Wiele osób mówi również o narzutu łączenia łańcuchów w metodach rejestrowania. Jednak rejestrowanie oparte na szablonie pozwala uniknąć tego kosztu i istnieje również w lipcu. Osobiście nigdy tak naprawdę nie piszę rejestrowania opartego na szablonach. Zbyt leniwy z tego powodu. Na przykład, jeśli zrobię to z JUL:
log.finest("Lookup request from username=" + username + ", valueX=" + valueX + ", valueY=" + valueY));
moje IDE ostrzeże mnie i poprosi o pozwolenie, aby zmienić je na:
log.log(Level.FINEST, "Lookup request from username={0}, valueX={1}, valueY={2}", new Object[]{username, valueX, valueY});
.. które oczywiście zaakceptuję. Pozwolenie udzielone ! Dziękuję za pomoc
Więc tak naprawdę nie piszę takich instrukcji, co robi IDE.
Podsumowując na temat wydajności, nie znalazłem niczego, co sugerowałoby, że wydajność JUL-a nie jest odpowiednia w porównaniu z konkurencją.
Konfiguracja ze ścieżki klasy . JUL po wyjęciu z pudełka nie może załadować pliku konfiguracyjnego ze ścieżki klasy. Aby to zrobić, wystarczy kilka linii kodu . Rozumiem, dlaczego może to być denerwujące, ale rozwiązanie jest krótkie i proste.
Dostępność programów obsługi wyjścia . JUL jest dostarczany z 5 modułami wyjściowymi: konsolą, strumieniem plików, gniazdem i pamięcią. Można je rozszerzyć lub napisać nowe. Może to być na przykład zapisywanie w dzienniku zdarzeń systemu UNIX / Linux i dzienniku zdarzeń systemu Windows. Osobiście nigdy nie miałem tego wymogu ani go nie widziałem, ale z pewnością mogę odnieść się do tego, dlaczego może to być przydatna funkcja. Logback jest dostarczany na przykład z aplikacją do Syslog. Nadal bym to argumentował
- 99,5% zapotrzebowania na miejsca docelowe produkcji jest pokryte tym, co znajduje się w JUL od razu po wyjęciu z pudełka.
- Specjalne potrzeby mogą być zaspokojone przez niestandardowe programy obsługi na JUL, a nie na czymś innym. Nic dla mnie nie sugeruje, że napisanie procedury obsługi wyjścia Syslog dla JUL zajmuje więcej czasu niż w przypadku innej struktury rejestrowania.
Naprawdę martwię się, że coś przeoczyłem. Korzystanie z fasad logowania i implementacji logowania innych niż JUL jest tak powszechne, że muszę dojść do wniosku, że to ja po prostu nie rozumiem. Obawiam się, że to nie byłby pierwszy raz. :-)
Więc co powinienem zrobić z moim API? Chcę, żeby odniosło sukces. Mogę oczywiście po prostu „płynąć z prądem” i wdrożyć SLF4J (który wydaje się obecnie najbardziej popularny), ale dla własnego dobra wciąż muszę dokładnie zrozumieć, co jest nie tak z dzisiejszym JUL, który gwarantuje wszystkie kłopoty? Czy sabotuję się, wybierając JUL dla mojej biblioteki?
Testowanie wydajności
(sekcja dodana przez nolan600 w dniu 07-JUL-2012)
Poniżej znajduje się odniesienie od Ceki o parametryzacji SLF4J 10 lub więcej razy szybciej niż JUL. Więc zacząłem robić proste testy. Na pierwszy rzut oka twierdzenie jest z pewnością słuszne. Oto wstępne wyniki (ale czytaj dalej!):
- Czas wykonania SLF4J, backend Logback: 1515
- Czas wykonania SLF4J, backend JUL: 12938
- Czas realizacji LIPIEC: 16911
Powyższe liczby to ms, więc im mniej, tym lepiej. Tak więc 10-krotna różnica w wydajności jest na początku całkiem bliska. Moja początkowa reakcja: to dużo!
Oto rdzeń testu. Jak widać liczba całkowita i ciąg znaków są konstruowane w pętli, która jest następnie używana w instrukcji log:
for (int i = 0; i < noOfExecutions; i++) {
for (char x=32; x<88; x++) {
String someString = Character.toString(x);
// here we log
}
}
(Chciałem, aby instrukcja dziennika miała zarówno prymitywny typ danych (w tym przypadku int), jak i bardziej złożony typ danych (w tym przypadku ciąg). Nie jestem pewien, czy to ważne, ale masz go.)
Instrukcja dziennika dla SLF4J:
logger.info("Logging {} and {} ", i, someString);
Instrukcja dziennika dla JUL:
logger.log(Level.INFO, "Logging {0} and {1}", new Object[]{i, someString});
JVM został „rozgrzany” tym samym testem wykonanym jeden raz przed faktycznym pomiarem. Java 1.7.03 był używany w Windows 7. Użyto najnowszych wersji SLF4J (v1.6.6) i Logback (v1.0.6). Stdout i stderr zostały przekierowane na urządzenie zerowe.
Jednak uważaj teraz, okazuje się, że JUL spędza większość czasu, getSourceClassName()
ponieważ JUL domyślnie drukuje nazwę klasy źródłowej na wyjściu, podczas gdy Logback nie. Porównujemy więc jabłka i pomarańcze. Muszę ponownie wykonać test i skonfigurować implementacje rejestrowania w podobny sposób, aby faktycznie generowały te same rzeczy. Podejrzewam jednak, że SLF4J + Logback nadal będzie na topie, ale daleko mu do początkowych liczb, jak podano powyżej. Bądźcie czujni.
Btw: Test po raz pierwszy faktycznie współpracowałem z SLF4J lub Logback. Przyjemne doświadczenie. JUL z pewnością jest o wiele mniej przyjazny, gdy zaczynasz.
Testowanie wydajności (część 2)
(sekcja dodana przez nolan600 w dniu 08-JUL-2012)
Jak się okazuje, tak naprawdę nie ma znaczenia wydajność, w jaki sposób konfigurujesz swój wzorzec w JUL, tj. Czy zawiera nazwę źródła, czy nie. Próbowałem z bardzo prostym wzorem:
java.util.logging.SimpleFormatter.format="%4$s: %5$s [%1$tc]%n"
i to wcale nie zmieniło powyższych czasów. Mój profiler ujawnił, że logger nadal spędzał dużo czasu na rozmowach telefonicznych, getSourceClassName()
nawet jeśli nie było to częścią mojego wzorca. Wzór nie ma znaczenia.
W związku z tym dochodzę do wniosku, że przynajmniej w przypadku testowanej instrukcji dziennika opartej na szablonie wydaje się, że w rzeczywistej różnicy wydajności między JUL (powolnym) a SLF4J + Logback (szybkim) jest około 10. Tak jak powiedział Ceki.
Widzę też inną rzecz, mianowicie to, że getLogger()
połączenie SLF4J jest znacznie droższe niż JUL ditto. (95 ms vs 0,3 ms, jeśli mój profiler jest dokładny). To ma sens. SLF4J musi poświęcić trochę czasu na powiązanie podstawowej implementacji rejestrowania. To mnie nie przeraża. Połączenia te powinny być dość rzadkie w trakcie życia aplikacji. Szybkość powinna znajdować się w rzeczywistych wywołaniach dziennika.
Ostateczna konkluzja
(sekcja dodana przez nolan600 w dniu 08-JUL-2012)
Dziękuję za wszystkie odpowiedzi. W przeciwieństwie do tego, co początkowo myślałem, ostatecznie zdecydowałem się użyć SLF4J dla mojego API. Jest to oparte na wielu rzeczach i twoim wkładzie:
Daje elastyczność wyboru implementacji dziennika w czasie wdrażania.
Problemy z brakiem elastyczności konfiguracji JUL podczas uruchamiania na serwerze aplikacji.
SLF4J jest z pewnością znacznie szybszy, jak opisano powyżej, szczególnie jeśli połączysz go z Logbackiem. Nawet jeśli był to tylko trudny test, mam powody sądzić, że o wiele więcej wysiłku włożono w optymalizację SLF4J + Logback niż JUL.
Dokumentacja. Dokumentacja SLF4J jest po prostu o wiele bardziej wyczerpująca i precyzyjna.
Elastyczność wzoru. Podczas testów postanowiłem, że JUL naśladuje domyślny wzorzec z Logback. Ten wzór zawiera nazwę wątku. Okazuje się, że JUL nie może tego zrobić po wyjęciu z pudełka. Ok, nie przegapiłem tego do tej pory, ale nie sądzę, że jest to rzecz, której powinno brakować w logach. Kropka!
Większość (lub wiele) projektów Java korzysta obecnie z Maven, więc dodanie zależności nie jest aż tak duże, szczególnie jeśli ta zależność jest raczej stabilna, tj. Nie zmienia ciągle interfejsu API. Wydaje się, że tak jest w przypadku SLF4J. Również słoik SLF4J i przyjaciele są małe.
Dziwną rzeczą, która się wydarzyła, było to, że naprawdę bardzo się zdenerwowałem JUL po tym, jak trochę popracowałem nad SLF4J. Nadal żałuję, że tak musi być z JUL. JUL jest daleki od ideału, ale w pewnym sensie spełnia swoje zadanie. Po prostu nie dość dobrze. To samo można powiedzieć Properties
jako przykład, ale nie myślimy o abstrakcji, aby ludzie mogli podłączyć własną bibliotekę konfiguracji i co masz. Myślę, że powodem jest to, że Properties
pojawia się tuż nad poprzeczką, podczas gdy odwrotnie jest w przypadku JUL dzisiejszego dnia ... a w przeszłości było zero, ponieważ nie istniało.
java.lang.System.Logger
, który jest interfejsem , który można przekierować do dowolnej faktycznej struktury rejestrowania, o ile ma ona miejsce i zapewnia implementację tego interfejsu. W połączeniu z modularyzacją możesz nawet wdrożyć aplikację z pakietem JRE niezawierającym java.util.logging
, jeśli wolisz inną strukturę.