Czy program może polegać na bibliotece podczas kompilacji, ale nie w czasie wykonywania?

110

Rozumiem różnicę między środowiskiem uruchomieniowym i czasem kompilacji oraz jak je rozróżnić, ale po prostu nie widzę potrzeby rozróżniania między zależnościami czasu kompilacji i czasu wykonania .

Dławię się tym: w jaki sposób program może nie zależeć w czasie wykonywania od czegoś, od czego zależał podczas kompilacji? Jeśli moja aplikacja Java używa log4j, to potrzebuje pliku log4j.jar w celu kompilacji (mój kod integruje się z metodami składowymi i wywołuje je z wnętrza log4j), a także środowiska uruchomieniowego (mój kod nie ma absolutnie żadnej kontroli nad tym, co się stanie, gdy kod wewnątrz log4j .jar jest uruchomiony).

Czytam o narzędziach do rozwiązywania zależności, takich jak Ivy i Maven, i te narzędzia wyraźnie rozróżniają te dwa typy zależności. Po prostu nie rozumiem takiej potrzeby.

Czy ktokolwiek może podać proste wyjaśnienie typu „King's English”, najlepiej z rzeczywistym przykładem, który nawet taki biedny głupek jak ja mógłby zrozumieć?

IAmYourFaja
źródło
2
Możesz użyć odbicia i klas, które nie były dostępne w czasie kompilacji. Pomyśl „wtyczka”.
Per Alexandersson

Odpowiedzi:

64

Zależność od czasu kompilacji jest zwykle wymagana w czasie wykonywania. W maven compilezależność o określonym zakresie zostanie dodana do ścieżki klasy w czasie wykonywania (np. W wojnach zostaną skopiowane do WEB-INF / lib).

Nie jest to jednak ściśle wymagane; na przykład możemy kompilować w oparciu o określony interfejs API, czyniąc z niego zależność w czasie kompilacji, ale w czasie wykonywania możemy dołączyć implementację, która również zawiera API.

Mogą wystąpić skrajne przypadki, w których projekt wymaga pewnej zależności do skompilowania, ale wtedy odpowiedni kod nie jest w rzeczywistości potrzebny, ale będą one rzadkie.

Z drugiej strony, dołączanie zależności środowiska uruchomieniowego, które nie są potrzebne w czasie kompilacji, jest bardzo powszechne. Na przykład, pisząc aplikację Java EE 6, kompilujesz w oparciu o API Java EE 6, ale w czasie wykonywania można użyć dowolnego kontenera Java EE; to ten kontener zapewnia implementację.

Zależności w czasie kompilacji można uniknąć, używając odbicia. Na przykład sterownik JDBC można załadować za pomocą, Class.forNamea rzeczywistą ładowaną klasę można konfigurować za pomocą pliku konfiguracyjnego.

Artefacto
źródło
17
O API Java EE - czy nie do tego służy „dostarczony” zakres zależności?
Kevin
15
Przykładem, w którym zależność jest wymagana do kompilacji, ale nie jest potrzebna w czasie wykonywania, jest lombok (www.projectlombok.org). Plik jar służy do przekształcania kodu Java w czasie kompilacji, ale nie jest w ogóle potrzebny w czasie wykonywania. Określenie zakresu „podany” powoduje, że jar nie zostanie uwzględniony w war / jar.
Kevin
2
@Kevin Tak, słuszna uwaga, providedzakres dodaje zależność od czasu kompilacji bez dodawania zależności od środowiska wykonawczego, przy założeniu, że zależność zostanie dostarczona w czasie wykonywania innymi środkami (np. Udostępniona biblioteka w kontenerze). runtimez drugiej strony dodaje zależność od czasu wykonania, nie czyniąc jej zależnością od czasu kompilacji.
Artefacto
Czy można więc bezpiecznie powiedzieć, że zazwyczaj istnieje korelacja 1: 1 między „konfiguracją modułu” (przy użyciu terminów Ivy) a głównym katalogiem w katalogu głównym projektu? Na przykład wszystkie moje testy JUnit, które zależą od JUnit JAR, będą znajdować się w katalogu test / root, itp. Po prostu nie widzę, jak te same klasy, spakowane w tym samym źródłowym katalogu głównym, mogłyby być „skonfigurowane”, aby zależały od różnych JAR w dowolnym momencie. Jeśli potrzebujesz log4j, potrzebujesz log4j; nie ma sposobu, aby powiedzieć temu samemu kodowi, aby wywoływał wywołania log4j w jednej konfiguracji, ale ignorował wywołania log4j w jakiejś konfiguracji „niezalogowanej”, prawda?
IAmYourFaja
30

Każda zależność Maven ma zakres, który definiuje, w której ścieżce klas jest dostępna ta zależność.

Podczas tworzenia pliku JAR dla projektu zależności nie są pakowane w generowany artefakt; są używane tylko do kompilacji. (Jednak nadal możesz zmusić mavena do uwzględnienia zależności w zbudowanym pliku jar , zobacz: Dołączanie zależności w pliku jar za pomocą Mavena )

W przypadku używania Mavena do tworzenia pliku WAR lub EAR można skonfigurować Maven do łączenia zależności z wygenerowanym artefaktem, a także można go skonfigurować tak, aby wykluczył pewne zależności z pliku WAR przy użyciu podanego zakresu.

Najczęstszy zakres - Zakres kompilacji - wskazuje, że zależność jest dostępna dla projektu w ścieżce klas kompilacji, ścieżkach klas kompilacji i wykonywania testów jednostkowych oraz ostatecznej ścieżce klas środowiska uruchomieniowego podczas wykonywania aplikacji. W aplikacji internetowej Java EE oznacza to, że zależność jest kopiowana do wdrożonej aplikacji. Jednak w pliku .jar zależności nie zostaną uwzględnione w zakresie kompilacji.

Zakres czasu wykonywania wskazuje, że zależność jest dostępna dla projektu od wykonywania testów jednostkowych i ścieżek klas wykonywania w czasie wykonywania, ale w przeciwieństwie do zakresu kompilacji nie jest dostępna podczas kompilowania aplikacji lub jej testów jednostkowych. Zależność wykonawcza jest kopiowana do wdrożonej aplikacji, ale nie jest dostępna podczas kompilacji! Jest to dobre, aby upewnić się, że nie polegasz omyłkowo na konkretnej bibliotece.

Na koniec podany zakres wskazuje, że kontener, w którym jest wykonywana aplikacja, zapewnia zależność w Twoim imieniu. W przypadku aplikacji Java EE oznacza to, że zależność jest już zależna od ścieżki klas kontenera serwletu lub serwera aplikacji i nie jest kopiowana do wdrożonej aplikacji. Oznacza to również, że potrzebujesz tej zależności do kompilowania projektu.

Koray Tugay
źródło
@Koray Tugay Odpowiedź jest bardziej precyzyjna :) Mam szybkie pytanie, mówię, że mam słoik zależności z zakresem czasu wykonywania. Czy maven będzie szukał słoika w czasie kompilacji?
gks
@gks Nie, nie będzie tego wymagać w czasie kompilacji.
Koray Tugay
9

Potrzebujesz zależności w czasie kompilacji, które mogą być potrzebne w czasie wykonywania. Jednak wiele bibliotek działa bez wszystkich możliwych zależności. tj. biblioteki, które mogą korzystać z czterech różnych bibliotek XML, ale do działania potrzebuje tylko jednej.

Wiele bibliotek potrzebuje po kolei innych bibliotek. Te biblioteki nie są potrzebne w czasie kompilacji, ale są potrzebne w czasie wykonywania. tj. kiedy kod jest faktycznie wykonywany.

Peter Lawrey
źródło
czy możesz podać przykłady takich bibliotek, które nie będą potrzebne podczas kompilacji, ale będą potrzebne w czasie wykonywania?
Cristiano,
1
@Cristiano wszystkie biblioteki JDBC są takie. Również biblioteki, które implementują standardowe API.
Peter Lawrey
4

Ogólnie masz rację i prawdopodobnie jest to idealna sytuacja, jeśli zależności czasu wykonywania i czasu kompilacji są identyczne.

Podam 2 przykłady, kiedy ta zasada jest niepoprawna.

Jeśli klasa A zależy od klasy B, która zależy od klasy C, która zależy od klasy D, gdzie A jest twoją klasą, a B, C i D są klasami z różnych bibliotek zewnętrznych, potrzebujesz tylko B i C w czasie kompilacji i potrzebujesz również D w runtime. Często programy używają dynamicznego ładowania klas. W tym przypadku nie potrzebujesz klas dynamicznie ładowanych przez bibliotekę, której używasz w czasie kompilacji. Ponadto często biblioteka wybiera, której implementacji użyć w czasie wykonywania. Na przykład SLF4J lub Commons Logging może zmienić implementację dziennika docelowego w czasie wykonywania. W czasie kompilacji potrzebujesz tylko samego SSL4J.

Przeciwny przykład, gdy potrzebujesz więcej zależności w czasie kompilacji niż w czasie wykonywania. Pomyśl, że tworzysz aplikację, która ma działać w różnych środowiskach lub systemach operacyjnych. Potrzebujesz wszystkich bibliotek specyficznych dla platformy w czasie kompilacji i tylko bibliotek potrzebnych do bieżącego środowiska w czasie wykonywania.

Mam nadzieję, że moje wyjaśnienia pomogą.

AlexR
źródło
Czy możesz wyjaśnić, dlaczego C jest potrzebne w czasie kompilacji w twoim przykładzie? Mam wrażenie (ze stackoverflow.com/a/7257518/6095334 ), że to, czy C jest potrzebne w czasie kompilacji, zależy od tego, do jakich metod i pól (z B) odwołuje się A.
Hervian
3

Zwykle wykres zależności statycznych jest pod-grafem wykresu dynamicznego, patrz np. Ten wpis na blogu autora NDepend .

To powiedziawszy, jest kilka wyjątków, głównie zależności, które dodają obsługę kompilatora, która staje się niewidoczna w czasie wykonywania. Na przykład do generowania kodu za pośrednictwem Lombok lub dodatkowych sprawdzeń, jak za pośrednictwem (podłączanego typu) Checker Framework .

DaveFar
źródło
2

Właśnie napotkałem problem, który odpowiada na Twoje pytanie. servlet-api.jarjest przejściową zależnością w moim projekcie sieci Web i jest potrzebny zarówno w czasie kompilacji, jak i w czasie wykonywania. Ale servlet-api.jarjest również zawarte w mojej bibliotece Tomcat.

Rozwiązaniem jest tutaj udostępnienie servlet-api.jarw mavenie tylko w czasie kompilacji i nie umieszczanie go w moim pliku wojennym, aby nie kolidował z servlet-api.jarzawartym w mojej bibliotece Tomcat.

Mam nadzieję, że to wyjaśnia zależność czasu kompilacji i czasu wykonywania.

Mayoor
źródło
3
Twój przykład jest w rzeczywistości nieprawidłowy dla danego pytania, ponieważ wyjaśnia różnicę między compilei providedzakresami, a nie między compilei runtime. Compile scopejest potrzebny w czasie kompilacji i jest spakowany w Twojej aplikacji. Provided scopejest potrzebny tylko w czasie kompilacji, ale nie jest spakowany w aplikacji, ponieważ jest dostarczany przez inne sposoby, na przykład jest już na serwerze Tomcat.
MJar
1
Cóż, myślę, że jest to raczej dobry przykład, ponieważ pytanie dotyczyło zależności czasu kompilacji i środowiska uruchomieniowego, a nie zakresówcompile i runtime maven . providedZakres jest droga Maven uchwyty przypadku gdy zależność czasu kompilacji nie powinny być włączone do pakietu uruchomieniowego.
Christian Gawron
1

Rozumiem różnicę między środowiskiem uruchomieniowym i czasem kompilacji oraz jak je rozróżnić, ale po prostu nie widzę potrzeby rozróżniania między zależnościami czasu kompilacji i czasu wykonania.

Ogólne koncepcje czasu kompilacji i środowiska uruchomieniowego compileoraz runtimezależności specyficzne i zakresowe Mavena to dwie bardzo różne rzeczy. Nie można ich bezpośrednio porównywać, ponieważ nie mają one tej samej ramki: ogólne koncepcje kompilacji i środowiska uruchomieniowego są szerokie, podczas gdy koncepcje maven compilei runtimezakresu dotyczą konkretnie dostępności / widoczności zależności w zależności od czasu: kompilacja lub wykonanie.
Nie zapominaj, że Maven to przede wszystkim javac/ javawrapper, a w Javie masz ścieżkę klasy czasu kompilacji, którą określasz, javac -cp ... i ścieżkę klas środowiska wykonawczego, którą określasz java -cp ....
Nie byłoby błędem traktowanie compilezakresu Maven jako sposobu na dodanie zależności zarówno w kompilacji Java, jak iw ścieżce klasy środowiska wykonawczego (javacand java), podczas gdy runtimezakres Maven może być postrzegany jako sposób na dodanie zależności tylko w środowisku wykonawczym Java classppath ( javac).

Dławię się tym: w jaki sposób program może nie zależeć w czasie wykonywania od czegoś, od czego zależał podczas kompilacji?

To, co opisujesz, nie ma żadnego związku runtimeani compilezakresu.
Wygląda na to, że bardziej odpowiada providedzakresowi, który określisz, aby zależność zależała od tego w czasie kompilacji, ale nie w czasie wykonywania.
Używasz go, ponieważ potrzebujesz zależności do kompilacji, ale nie chcesz włączać go do pakietu (JAR, WAR lub innych), ponieważ zależność jest już dostarczona przez środowisko: może być zawarta na serwerze lub w dowolnym ścieżka do ścieżki klasy określonej podczas uruchamiania aplikacji Java.

Jeśli moja aplikacja Java używa log4j, to potrzebuje pliku log4j.jar w celu kompilacji (mój kod integruje się z metodami składowymi i wywołuje je z wnętrza log4j), a także środowiska uruchomieniowego (mój kod nie ma absolutnie żadnej kontroli nad tym, co się stanie, gdy kod wewnątrz log4j .jar jest uruchomiony).

W tym przypadku tak. Ale przypuśćmy, że musisz napisać przenośny kod, który opiera się na slf4j jako fasadzie przed log4j, aby móc później przełączyć się na inną implementację rejestrowania (log4J 2, logback lub dowolną inną).
W tym przypadku w twoim pomie musisz określić slf4j jako compilezależność (jest to ustawienie domyślne), ale określisz zależność log4j jako runtimezależność:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>...</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>...</version>
    <scope>runtime</scope>
</dependency>

W ten sposób nie można było odwoływać się do klas log4j w skompilowanym kodzie, ale nadal będzie można odwoływać się do klas slf4j.
Jeśli określisz te dwie zależności z compileczasem, nic nie powstrzyma cię przed odwoływaniem się do klas log4j w skompilowanym kodzie i możesz stworzyć niepożądane sprzężenie z implementacją logowania:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>...</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>...</version>
</dependency>

Typowym zastosowaniem runtimezakresu jest deklaracja zależności JDBC. Aby napisać kod przenośny, nie chcesz, aby kod klienta mógł odwoływać się do klas określonej zależności DBMS (na przykład: zależność PostgreSQL JDBC), ale chcesz, aby to samo zawierało go w aplikacji, ponieważ w czasie wykonywania klasy są potrzebne do API JDBC współpracuje z tym systemem DBMS.

davidxxx
źródło
0

W czasie kompilacji włączasz kontrakty / API, których oczekujesz od swoich zależności. (np .: tutaj po prostu podpisujesz umowę z dostawcą szerokopasmowego Internetu) W rzeczywistości używasz zależności. (np .: faktycznie korzystasz z szerokopasmowego internetu)

Nicolae Dascalu
źródło
0

Aby odpowiedzieć na pytanie „w jaki sposób program może nie zależeć w czasie wykonywania od czegoś, od czego zależał podczas kompilacji?”, Spójrzmy na przykład procesora adnotacji.

Załóżmy, że napisałeś własny procesor adnotacji i przypuśćmy, że ma on zależność od czasu kompilacji, com.google.auto.service:auto-servicedzięki czemu może używać @AutoService. Ta zależność jest wymagana tylko do kompilowania procesora adnotacji, ale nie jest wymagana w czasie wykonywania: wszystkie inne projekty zależne od procesora adnotacji do przetwarzania adnotacji nie wymagają zależności odcom.google.auto.service:auto-service w czasie wykonywania (ani w czasie kompilacji ani w żadnym innym czasie) .

Nie jest to zbyt częste, ale zdarza się.

user23288
źródło
0

Plik runtimeZakres jest tam, aby zapobiec programistów dodawanie bezpośrednie zależności do biblioteki implementacji w kodzie zamiast korzystania abstrakcje lub elewacjach.

Innymi słowy, wymusza korzystanie z interfejsów.

Konkretne przykłady:

1) Twój zespół używa SLF4J zamiast Log4j. Chcesz, aby twoi programiści używali SLF4J API, a nie Log4j. Log4j może być używany tylko przez SLF4J wewnętrznie. Rozwiązanie:

  • Zdefiniuj SLF4J jako zwykłą zależność w czasie kompilacji
  • Zdefiniuj log4j-core i log4j-api jako zależności środowiska wykonawczego.

2) Twoja aplikacja uzyskuje dostęp do MySQL za pomocą JDBC. Chcesz, aby programiści tworzyli kod w oparciu o standardową abstrakcję JDBC, a nie bezpośrednio przeciwko implementacji sterownika MySQL.

  • Zdefiniuj mysql-connector-java(sterownik MySQL JDBC) jako zależność środowiska wykonawczego.

Zależności środowiska wykonawczego są ukryte podczas kompilacji (generują błędy w czasie kompilacji, jeśli kod ma od nich „bezpośrednią” zależność), ale są uwzględniane w czasie wykonywania i podczas tworzenia artefaktów możliwych do wdrożenia (pliki WAR, pliki JAR SHADED itp.).

Agustí Sánchez
źródło