Un programme peut-il dépendre d'une bibliothèque lors de la compilation mais pas à l'exécution?

110

Je comprends la différence entre runtime et compile-time et comment faire la différence entre les deux, mais je ne vois tout simplement pas la nécessité de faire une distinction entre les dépendances de compilation et d'exécution .

Ce dont je m'étouffe, c'est ceci: comment un programme peut-il ne pas dépendre de quelque chose à l'exécution dont il dépendait lors de la compilation? Si mon application Java utilise log4j, elle a besoin du fichier log4j.jar pour compiler (mon code s'intégrant avec et invoquant des méthodes membres depuis log4j) ainsi que runtime (mon code n'a absolument aucun contrôle sur ce qui se passe une fois le code dans log4j .jar est exécuté).

Je lis sur les outils de résolution de dépendances tels que Ivy et Maven, et ces outils font clairement la distinction entre ces deux types de dépendances. Je n'en comprends tout simplement pas la nécessité.

Quelqu'un peut-il donner une explication simple de type "King's English", de préférence avec un exemple réel que même une pauvre sève comme moi pourrait comprendre?

IAmYourFaja
la source
2
Vous pouvez utiliser la réflexion et utiliser des classes qui n'étaient pas disponibles au moment de la compilation. Pensez "plugin".
Per Alexandersson

Réponses:

64

Une dépendance à la compilation est généralement requise au moment de l'exécution. Dans maven, une compiledépendance de portée sera ajoutée au chemin de classe lors de l'exécution (par exemple, dans wars, elles seront copiées dans WEB-INF / lib).

Ce n'est cependant pas strictement obligatoire; par exemple, nous pouvons compiler avec une certaine API, ce qui en fait une dépendance au moment de la compilation, mais ensuite, au moment de l'exécution, inclure une implémentation qui inclut également l'API.

Il peut y avoir des cas marginaux où le projet nécessite une certaine dépendance pour être compilé, mais le code correspondant n'est pas réellement nécessaire, mais ceux-ci seront rares.

D'un autre côté, l'inclusion de dépendances d'exécution qui ne sont pas nécessaires au moment de la compilation est très courante. Par exemple, si vous écrivez une application Java EE 6, vous compilez avec l'API Java EE 6, mais au moment de l'exécution, n'importe quel conteneur Java EE peut être utilisé; c'est ce conteneur qui fournit l'implémentation.

Les dépendances au moment de la compilation peuvent être évitées en utilisant la réflexion. Par exemple, un pilote JDBC peut être chargé avec a Class.forNameet la classe réelle chargée peut être configurée via un fichier de configuration.

Artefacto
la source
17
À propos de l'API Java EE - n'est-ce pas à cela que sert la portée de dépendance «fournie»?
Kevin
15
lombok (www.projectlombok.org) est un exemple où une dépendance est nécessaire pour la compilation mais non nécessaire à l'exécution. Le jar est utilisé pour transformer le code java au moment de la compilation mais n'est pas du tout nécessaire au moment de l'exécution. Spécifier la portée «fournie» fait que le jar n'est pas inclus dans war / jar.
Kevin
2
@Kevin Oui, bon point, la providedportée ajoute une dépendance au moment de la compilation sans ajouter de dépendance à l'exécution dans l'espoir que la dépendance sera fournie à l'exécution par d'autres moyens (par exemple une bibliothèque partagée dans le conteneur). runtimed'autre part, ajoute une dépendance d'exécution sans en faire une dépendance à la compilation.
Artefacto
Est-il donc prudent de dire qu'il existe généralement une corrélation 1: 1 entre une "configuration de module" (en utilisant les termes Ivy) et le répertoire principal sous la racine de votre projet? Par exemple, tous mes tests JUnit qui dépendent du JAR JUnit seront sous test / root, etc. Je ne vois tout simplement pas comment les mêmes classes, conditionnées sous la même racine source, pourraient être "configurées" pour dépendre de différentes JAR à tout moment. Si vous avez besoin de log4j, alors vous avez besoin de log4j; il n'y a aucun moyen de dire au même code d'appeler les appels log4j sous 1 config, mais d'ignorer les appels log4j sous une configuration "non-logging", non?
IAmYourFaja
30

Chaque dépendance Maven a une portée qui définit le chemin de classe sur lequel cette dépendance est disponible.

Lorsque vous créez un JAR pour un projet, les dépendances ne sont pas regroupées avec l'artefact généré; ils ne sont utilisés que pour la compilation. (Cependant, vous pouvez toujours faire en sorte que maven inclue les dépendances dans le jar construit, voir: Inclure des dépendances dans un jar avec Maven )

Lorsque vous utilisez Maven pour créer un fichier WAR ou EAR, vous pouvez configurer Maven pour regrouper les dépendances avec l'artefact généré, et vous pouvez également le configurer pour exclure certaines dépendances du fichier WAR en utilisant la portée fournie.

L'étendue la plus courante - Compile Scope - indique que la dépendance est disponible pour votre projet sur le chemin de classe de compilation, les chemins de classe de compilation et d'exécution du test unitaire, et le chemin de classe d'exécution éventuel lorsque vous exécutez votre application. Dans une application Web Java EE, cela signifie que la dépendance est copiée dans votre application déployée. Cependant, dans un fichier .jar, les dépendances ne seront pas incluses dans la portée de compilation.

L'étendue d'exécution indique que la dépendance est disponible pour votre projet sur les chemins de classe d'exécution de test unitaire et d'exécution d'exécution, mais contrairement à l'étendue de compilation, elle n'est pas disponible lorsque vous compilez votre application ou ses tests unitaires. Une dépendance d'exécution est copiée dans votre application déployée, mais elle n'est pas disponible lors de la compilation! C'est bon pour vous assurer de ne pas dépendre par erreur d'une bibliothèque spécifique.

Enfin, Provided Scope indique que le conteneur dans lequel votre application s'exécute fournit la dépendance en votre nom. Dans une application Java EE, cela signifie que la dépendance est déjà sur le chemin de classe du conteneur de servlet ou du serveur d'application et n'est pas copiée dans votre application déployée. Cela signifie également que vous avez besoin de cette dépendance pour compiler votre projet.

Koray Tugay
la source
@Koray Tugay La réponse est plus précise :) J'ai une question rapide disant que j'ai un pot de dépendance avec la portée du temps d'exécution. Le maven recherchera-t-il le pot au moment de la compilation?
gks
@gks Non, cela ne sera pas nécessaire au moment de la compilation.
Koray Tugay
9

Vous avez besoin au moment de la compilation des dépendances dont vous pourriez avoir besoin au moment de l'exécution. Cependant, de nombreuses bibliothèques fonctionnent sans toutes ses dépendances possibles. c'est-à-dire une bibliothèque qui peut utiliser quatre bibliothèques XML différentes, mais n'en a besoin que d'une pour fonctionner.

De nombreuses bibliothèques ont besoin à leur tour d'autres bibliothèques. Ces bibliothèques ne sont pas nécessaires au moment de la compilation, mais sont nécessaires au moment de l'exécution. c'est-à-dire lorsque le code est réellement exécuté.

Peter Lawrey
la source
pouvez-vous nous donner des exemples de telles bibliothèques qui ne seront pas nécessaires à la compilation mais qui le seront au moment de l'exécution?
Cristiano
1
@Cristiano toutes les bibliothèques JDBC sont comme ça. Aussi des bibliothèques qui implémentent une API standard.
Peter Lawrey
4

En général, vous avez raison et c'est probablement la situation idéale si les dépendances d'exécution et de compilation sont identiques.

Je vais vous donner 2 exemple lorsque cette règle est incorrecte.

Si la classe A dépend de la classe B qui dépend de la classe C qui dépend de la classe D où A est votre classe et B, C et D sont des classes de différentes bibliothèques tierces, vous n'avez besoin que de B et C au moment de la compilation et vous avez également besoin de D à Durée. Les programmes utilisent souvent le chargement dynamique des classes. Dans ce cas, vous n'avez pas besoin de classes chargées dynamiquement par la bibliothèque que vous utilisez au moment de la compilation. De plus, la bibliothèque choisit souvent l'implémentation à utiliser au moment de l'exécution. Par exemple, SLF4J ou Commons Logging peut modifier l'implémentation du journal cible lors de l'exécution. Vous n'avez besoin que de SSL4J lui-même au moment de la compilation.

Exemple opposé lorsque vous avez besoin de plus de dépendances au moment de la compilation qu'au moment de l'exécution. Pensez que vous développez une application qui doit fonctionner dans différents environnements ou systèmes d'exploitation. Vous avez besoin de toutes les bibliothèques spécifiques à la plate-forme au moment de la compilation et uniquement des bibliothèques nécessaires pour l'environnement actuel au moment de l'exécution.

J'espère que mes explications vous aideront.

AlexR
la source
Pouvez-vous expliquer pourquoi C est nécessaire au moment de la compilation dans votre exemple? J'ai l'impression (de stackoverflow.com/a/7257518/6095334 ) que le fait que C soit nécessaire ou non au moment de la compilation dépend des méthodes et des champs (de B) A référencés.
Hervian
3

Habituellement, le graphe des dépendances statiques est un sous-graphe du graphe dynamique, voir par exemple cette entrée de blog de l'auteur de NDepend .

Cela dit, il y a quelques exceptions, principalement des dépendances qui ajoutent le support du compilateur, qui devient invisible à l'exécution. Par exemple pour la génération de code comme via Lombok ou des vérifications supplémentaires comme via le (type enfichable-) Checker Framework .

DaveFar
la source
2

Je viens de rencontrer un problème qui répond à votre question. servlet-api.jarest une dépendance transitoire dans mon projet Web et est nécessaire à la fois au moment de la compilation et à l'exécution. Mais servlet-api.jarest également inclus dans ma bibliothèque Tomcat.

La solution ici est de rendre servlet-api.jarin maven disponible uniquement au moment de la compilation et non empaqueté dans mon fichier war afin qu'il n'entre pas en conflit avec le servlet-api.jarcontenu de ma bibliothèque Tomcat.

J'espère que cela explique le temps de compilation et la dépendance à l'exécution.

Mayoor
la source
3
Votre exemple est en fait incorrecte pour la question donnée, car il explique la différence entre compileet providedchamps et non entre compileet runtime. Compile scopeest à la fois nécessaire au moment de la compilation et est intégré à votre application. Provided scopen'est nécessaire qu'au moment de la compilation, mais n'est pas emballé dans votre application car il est fourni par un autre moyen, par exemple, il est déjà sur le serveur Tomcat.
MJar
1
Eh bien, je pense que c'est un plutôt bon exemple parce que la question était au sujet de compilation et d' exécution des dépendances et pas compileet les runtime étendues Maven . La providedportée est la façon dont maven gère le cas où une dépendance à la compilation ne doit pas être incluse dans le package d'exécution.
Christian Gawron
1

Je comprends la différence entre runtime et compile-time et comment faire la différence entre les deux, mais je ne vois tout simplement pas la nécessité de faire une distinction entre les dépendances de compilation et d'exécution.

Les concepts généraux de compilation et d'exécution et les dépendances spécifiques à Maven compileet à l' runtimeétendue sont deux choses très différentes. Vous ne pouvez pas les comparer directement car ils n'ont pas le même cadre: les concepts généraux de compilation et d'exécution sont larges tandis que les concepts maven compileet runtimescope concernent spécifiquement la disponibilité / visibilité des dépendances en fonction du moment: compilation ou exécution.
N'oubliez pas que Maven est avant tout un javac/ javawrapper et qu'en Java vous avez un chemin de classe au moment de la compilation que vous spécifiez avec javac -cp ... et un chemin de classe d'exécution que vous spécifiez avec java -cp ....
Il ne serait pas faux de considérer la compileportée Maven comme un moyen d'ajouter une dépendance à la fois dans la compilation Java et dans le classppath d'exécution (javacet java) tandis que la runtimeportée Maven peut être considérée comme un moyen d'ajouter une dépendance uniquement dans le classppath ( javac) du runtime Java .

Ce dont je m'étouffe, c'est ceci: comment un programme peut-il ne pas dépendre de quelque chose à l'exécution dont il dépendait lors de la compilation?

Ce que vous décrivez n'a aucun rapport avec runtimeet compileportée.
Cela ressemble plus à la providedportée que vous spécifiez pour qu'une dépendance dépende de celle au moment de la compilation, mais pas au moment de l'exécution.
Vous l'utilisez car vous avez besoin de la dépendance à compiler mais vous ne voulez pas l'inclure dans le composant packagé (JAR, WAR ou tout autre) car la dépendance est déjà fournie par l'environnement: elle peut être incluse dans le serveur ou tout autre chemin du chemin d'accès aux classes spécifié au démarrage de l'application Java.

Si mon application Java utilise log4j, elle a besoin du fichier log4j.jar pour compiler (mon code s'intégrant avec et invoquant des méthodes membres depuis log4j) ainsi que runtime (mon code n'a absolument aucun contrôle sur ce qui se passe une fois le code dans log4j .jar est exécuté).

Dans ce cas oui. Mais supposons que vous ayez besoin d'écrire un code portable qui s'appuie sur slf4j comme façade devant log4j pour pouvoir passer à une autre implémentation de journalisation plus tard (log4J 2, logback ou toute autre).
Dans ce cas, dans you pom, vous devez spécifier slf4j en tant que compiledépendance (c'est la valeur par défaut) mais vous spécifierez la dépendance log4j en tant que runtimedépendance:

<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>

De cette façon, les classes log4j n'ont pas pu être référencées dans le code compilé mais vous pourrez toujours faire référence aux classes slf4j.
Si vous avez spécifié les deux dépendances avec l' compileheure, rien ne vous empêchera de référencer les classes log4j dans le code compilé et vous pourriez ainsi créer un couplage indésirable avec l'implémentation de la journalisation:

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

La runtimedéclaration de dépendance JDBC est une utilisation courante de la portée. Pour écrire du code portable, vous ne voulez pas que le code client puisse faire référence aux classes de la dépendance SGBD spécifique (par exemple: dépendance JDBC PostgreSQL) mais vous voulez tout de même l'inclure dans votre application car au moment de l'exécution, les classes sont nécessaires pour créer l'API JDBC fonctionne avec ce SGBD.

davidxxx
la source
0

Au moment de la compilation, vous activez les contrats / API que vous attendez de vos dépendances. (par exemple: ici vous venez de signer un contrat avec un fournisseur d'accès Internet haut débit) Au moment de l'exécution, vous utilisez les dépendances. (par exemple: ici, vous utilisez effectivement Internet haut débit)

Nicolae Dascalu
la source
0

Pour répondre à la question "comment un programme peut-il ne pas dépendre à l'exécution de quelque chose dont il dépendait lors de la compilation?", Regardons l'exemple d'un processeur d'annotations.

Supposons que vous ayez écrit votre propre processeur d'annotations, et supposez qu'il ait une dépendance au moment de la compilation com.google.auto.service:auto-servicepour pouvoir l'utiliser @AutoService. Cette dépendance n'est requise que pour compiler le processeur d'annotations, mais elle n'est pas obligatoire au moment de l'exécution: tous les autres projets dépendant de votre processeur d'annotations pour le traitement des annotations ne nécessitent pas la dépendance sur com.google.auto.service:auto-serviceau moment de l'exécution (ni au moment de la compilation ni à aucun autre moment) .

Ce n'est pas très courant, mais cela arrive.

user23288
la source
0

La runtimeportée est là pour empêcher les programmeurs d'ajouter des dépendances directes aux bibliothèques d'implémentation dans le code au lieu d'utiliser des abstractions ou des façades.

En d'autres termes, il oblige à utiliser des interfaces.

Exemples concrets:

1) Votre équipe utilise SLF4J sur Log4j. Vous voulez que vos programmeurs utilisent l'API SLF4J, pas celle de Log4j. Log4j doit être utilisé par SLF4J uniquement en interne. Solution:

  • Définir SLF4J comme une dépendance régulière à la compilation
  • Définissez log4j-core et log4j-api comme dépendances d'exécution.

2) Votre application accède à MySQL à l'aide de JDBC. Vous voulez que vos programmeurs codent par rapport à l'abstraction standard JDBC, pas directement par rapport à l'implémentation du pilote MySQL.

  • Définissez mysql-connector-java(pilote MySQL JDBC) comme une dépendance d'exécution.

Les dépendances d'exécution sont masquées lors de la compilation (générant des erreurs de compilation si votre code a une dépendance "directe" sur elles) mais sont incluses lors de l'exécution et lors de la création d'artefacts déployables (fichiers WAR, fichiers JAR SHADED, etc.).

Agustí Sánchez
la source