[JAVA] Créer un JAR exécutable multi-projets avec Gradle

Objectif de cet article

Un rappel des étapes pour gérer un multi-projet à l'aide de l'outil de construction Gradle et créer un JAR exécutable en tant qu'artefact.

--Gestion de plusieurs projets --Création d'un JAR exécutable qui dépend d'un JAR externe (JAR qui inclut tous les JAR dépendants, pas fat-JAR)

Est le thème principal.

Contexte de la création d'article

J'ai eu l'opportunité de créer une application GUI avec JavaFX [^ JavaFX]. L'application est enfin distribuée au format EXE par un outil appelé javapackager [^ javapackager], mais au préalable, il était nécessaire de générer un JAR qui peut être démarré en double-cliquant.

[^ JavaFX]: 1 Présentation de JavaFX (version 8) (https://docs.oracle.com/javase/jp/8/javafx/get-started-tutorial/jfx-overview.htm) [^ javapackager]: package d'application autonome (https://docs.oracle.com/javase/jp/8/docs/technotes/guides/deploy/self-contained-packaging.html)

De plus, les applications devaient créer plusieurs types avec presque le même traitement interne et un comportement détaillé différent. Je voulais utiliser common.jar comme partie commune et app1.jar et app2.jar (selon common.jar) comme JAR d'entrée, mais j'ai regardé dans Jenkins dans l'entreprise et recherché un multi-projet avec une configuration similaire. Même quand je l'ai essayé, il n'y avait pas de script qui n'était terminé qu'avec Gradle. Il n'y avait qu'un script shell qui appelait le build.gradle commun, puis l'app1 build.gradle.

J'aurais pu atteindre le but en imitant le script shell, mais pour apprendre, j'ai décidé de créer un script complété avec Gradle.

Environnement de vérification

Créer un multi-projet et créer un JAR exécutable

Pour plus de simplicité, commençons par un seul projet d'application.

Vérifier le fonctionnement avec un minimum de multi-projets

Tout d'abord, créez un multi-projet minimal et appelez la tâche hello pour vérifier l'opération.

Créez des répertoires app, common, master directement sous le répertoire de travail, et créez build.gradle et settings.gradle directement sous master [^ tree command sur Mac]:

[^ Commande Tree sur Mac]: Commande Tree sur Mac-Qiita (http://qiita.com/kanuma1984/items/c158162adfeb6b217973)

master$ tree ../
../
├── app
├── common
└── master
    ├── build.gradle
    └── settings.gradle

Dans settings.gradle, spécifiez le répertoire que vous souhaitez reconnaître comme sous-projet. Le projet racine n'est pas spécifié, mais le répertoire nommé master est automatiquement reconnu comme projet racine, il n'est donc pas spécifié [^ Multi-project with Gradle]:

[^ Multi-projet avec Gradle]: Multi-projet avec Gradle-Qiita (http://qiita.com/shiena/items/371fe817c8fb6be2bb1e)

settings.gradle


includeFlat 'app', 'common'

Dans build.gradle, définissez une tâche hello dans le bloc ʻall projects` [Pourquoi utiliser ^ doLast]:

[Raison de l'utilisation de ^ doLast]: La méthode leftShift et l'opérateur` << ʻ sont obsolètes. (https://docs.gradle.org/3.2/release-notes#the-left-shift-operator-on-the-task-interface)

build.gradle


allprojects {
    task hello {
        doLast {
            println "I`m ${project.name}."
        }
    }
}

À ce stade, appelez la tâche «hello» dans le répertoire maître [signifiant ^ q]. Si vous voyez ce qui suit, c'est OK:

[Signification de ^ q]: L'option -q est utilisée pour rendre le résultat de println plus facile à voir.

master$ gradle -q hello
I`m master.
I`m app.
I`m common.

Au fait, si vous appelez la tâche «hello» dans le répertoire commun, seule la tâche commune sera appelée. Vous pouvez également utiliser cette propriété pour créer uniquement des projets spécifiques:

common$ gradle -q hello
I`m common.

Créer une application et un projet Eclipse

Ajoutez le plug-in Eclipse à votre sous-projet. Ajoutez ce qui suit après le bloc «tous les projets»:

build.gradle


…

subprojects {
    apply {
        plugin 'eclipse'
    }
}

Avec cela seul, vous pouvez utiliser la tâche ʻeclipse` et créer un projet Eclipse:

master$ gradle eclipse
:app:eclipseProject
:app:eclipse
:common:eclipseProject
:common:eclipse

BUILD SUCCESSFUL

Total time: 2.067 secs

Cependant, comme le répertoire src est réellement requis, je vais le générer avec la tâche gradle [raison de ne pas utiliser la tâche ^ init]. Ajoutez un plug-in Java et créez une tâche ʻinitSrcDirs` comme indiqué ci-dessous.

[Raison pour ne pas utiliser ^ init task]: Gradle est livré avec une tâche ʻinit`, mais je ne voulais pas créer de fichiers supplémentaires, donc je l'ai définie moi-même.

build.gradle


…

subprojects {
    apply {
        plugin 'eclipse'
        plugin 'java'
    }

    task initSrcDirs {
        doLast {
            sourceSets.all {
                java.srcDirs*.mkdirs()
                resources.srcDirs*.mkdirs()
            }
        }
    }
}

ʻLorsque vous exécutez la tâche initSrcDirs`, src / main / java, src / main / resources, etc. sont créés. Même si les fichiers existent déjà dans ces chemins, ils ne seront jamais vides, donc c'est sûr à utiliser:

master$ gradle initSrcDirs
:app:initSrcDirs
:common:initSrcDirs

BUILD SUCCESSFUL

Total time: 0.909 secs

master$ tree ../
../
├── app
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
│       └── test
│           ├── java
│           └── resources
├── common
│   └── src
│       ├── main
│       │   ├── java
│       │   └── resources
│       └── test
│           ├── java
│           └── resources
└── master
    ├── build.gradle
    └── settings.gradle

ʻInitSrcDirs` Décrit les variables apparues lors de la définition de la tâche.

--sourceSets est un système de répertoires tel que src / main / java défini par le plug-in Java. --mkdirs () appelle la méthode mkdirs de java.io.File car srcDirs est une liste de java.io.File.

Si vous êtes intéressé par une autre syntaxe (par exemple, * . de l'itération), veuillez vérifier la syntaxe de Groovy.

Si vous appelez à nouveau la tâche ʻeclipse avec le répertoire src créé, le chemin de classe passera également correctement (la tâche ʻeclipse Claspath qui n'existait pas auparavant est appelée):

master$ gradle eclipse
:app:eclipseClasspath
:app:eclipseJdt
:app:eclipseProject
:app:eclipse
:common:eclipseClasspath
:common:eclipseJdt
:common:eclipseProject
:common:eclipse

BUILD SUCCESSFUL

Total time: 0.994 secs

Rendre le projet d'application dépendant du projet commun

Passons à l'essentiel de ce que nous voulions réaliser avec la gestion multi-projets. Rendre l'application dépendante du commun.

Cas d'échec

Tout d'abord, afin de reproduire le cas d'échec, créez les deux classes suivantes sans modifier build.gradle. Constant.java est créé sous common et Main.java est créé sous app. Bien sûr, Eclipse ne reconnaît même pas les dépendances de l'autre, donc il ne peut pas être complété et vous obtenez une erreur:

Constant.java


package com.github.kazuma1989.dec10_2016.common;

public class Constant {
    public static final String NAME = "common";
}

Main.java


package com.github.kazuma1989.dec10_2016.app;

import com.github.kazuma1989.dec10_2016.common.Constant;

public class Main {
    public static void main(String[] args) {
        System.out.println(Constant.NAME);
    }
}

Si vous exécutez la tâche jar dans cet état, la construction échoue comme prévu. La tâche compileJava, qui précède la tâche jar, a échoué:

master$ gradle jar
:app:compileJava
/Users/kazuma/qiita/2016/dec10/app/src/main/java/com/github/kazuma1989/dec10_2016/app/Main.java:3:Erreur:Paquet com.github.kazuma1989.dec10_2016.commun n'existe pas
import com.github.kazuma1989.dec10_2016.common.Constant;
                                              ^
/Users/kazuma/qiita/2016/dec10/app/src/main/java/com/github/kazuma1989/dec10_2016/app/Main.java:7:Erreur:Impossible de trouver le symbole
        System.out.println(Constant.NAME);
                           ^
symbole:Constante variable
endroit:Classe principale
2 erreurs
:app:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.966 secs

Cas réussi

Ensuite, faites réussir la construction. Ajoutez ce qui suit à la fin du bloc subprojects pour définir la dépendance sur common:

build.gradle


…

subprojects {
    …

    //Paramètres autres que courants
    if (project.name in ['common']) return

    dependencies {
        compile project(':common')
    }
}

Cette fois, la tâche jar réussit et le JAR créé fonctionne correctement. La conversion JAR courante est exécutée en premier, et non dans l'ordre croissant des noms de projet:

master$ gradle jar
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 1.154 secs

master$ java -cp ../app/build/libs/app.jar com.github.kazuma1989.dec10_2016.app.Main
common

La tâche jar ne doit pas nécessairement être appelée depuis le maître, seule celle de l'application peut être appelée. Même dans ce cas, l'application sera JARisée après la compilation commune, donc elle réussira correctement:

master$ gradle clean :app:jar
:app:clean
:common:clean
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.938 secs

master$ cd ../app/

app$ gradle clean jar
:app:clean
:common:compileJava UP-TO-DATE
:common:processResources UP-TO-DATE
:common:classes UP-TO-DATE
:common:jar UP-TO-DATE
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.922 secs

Après avoir défini les dépendances, exécutez à nouveau la tâche ʻeclipse` pour éliminer l'erreur dans Eclipse et utilisez la complétion de code.

Ajouter un manifeste pour faire de l'application un fichier JAR exécutable

Définissez le manifeste pour la tâche jar et générez un fichier JAR exécutable.

Parfois ça marche, mais les bases échouent

Ajoutez simplement une instruction minimale à la fin de build.gradle et essayez quand même d'exécuter la tâche jar:

build.gradle


…

project(':app') {
    jar {
        manifest {
            attributes 'Main-Class': 'com.github.kazuma1989.dec10_2016.app.Main'
        }
    }
}
master$ gradle jar
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 3.177 secs

Lorsque vous appelez le JAR créé avec la commande java, la classe Main est appelée:

master$ java -jar ../app/build/libs/app.jar 
common

Mais cela a fonctionné. En effet, la classe Constant est introuvable à moins que les informations de dépendance de common.jar ne soient décrites dans le manifeste. Cette fois, la classe Main fait uniquement référence à la constante de chaîne Constante, il est donc probable que la valeur ait été conservée dans la classe Main au moment de la compilation.

Cas où vous définissez des dépendances sur common.jar et cela fonctionne toujours

Normalement, ce sera NoClassDefFoundError. La compilation réussit, mais échoue au moment de l'exécution.

Par exemple, voici le cas où la classe de base ʻAbstractApplication` est créée en commun et héritée par app.

master$ gradle jar
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 1.188 secs

master$ java -jar ../app/build/libs/app.jar 
Exception in thread "main" java.lang.NoClassDefFoundError: com/github/kazuma1989/dec10_2016/common/AbstractApplication
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at com.github.kazuma1989.dec10_2016.app.Main.main(Main.java:9)
Caused by: java.lang.ClassNotFoundException: com.github.kazuma1989.dec10_2016.common.AbstractApplication
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 13 more

master$ tree ../app/build/libs/
../app/build/libs/
└── app.jar

Pour résoudre ce problème, changez l'intérieur du bloc jar comme suit. Le manifeste a des "attributs" Chemin de la classe "et la tâche a une opération" copie ":

build.gradle


…

project(':app') {
    jar {
        manifest {
            attributes 'Main-Class': 'com.github.kazuma1989.dec10_2016.app.Main'
            attributes 'Class-Path': configurations.runtime.collect{it.name}.join(' ')
        }

        doLast {
            copy {
                from configurations.runtime
                into destinationDir
            }
        }
    }
}

configurations.runtime contient des informations sur les dépendances d'exécution du projet d'application. Il contient également des informations sur le projet commun spécifié comme compile dans le bloc dependencies. En effet, les dépendances de compilation sont également des dépendances d'exécution.

Dans ʻattributes'Class-Path', le contenu de configurations.runtimeest concaténé avec des délimiteurs d'espace. Ainsi, le contenu du manifeste seraClass-Path: common.jar (si vous avez ajouté des dépendances autres que common, ce sera quelque chose comme Class-Path: common.jar another.jar extra.jar`. ).

Le JAR qui dit «Class-Path: common.jar» dans le manifeste cherchera common.jar dans le même répertoire que lui-même, donc copiez common.jar dans le même répertoire que app.jar dans le bloc «copy». Je suis.

Avec l'ajout du manifeste et de la copie du JAR dépendant, l'exécution du JAR est désormais réussie. Si vous portez le répertoire libs, votre application est terminée:

master$ gradle clean jar
:app:clean
:common:clean
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.952 secs

master$ java -jar ../app/build/libs/app.jar 
common

master$ tree ../app/build/libs/
../app/build/libs/
├── app.jar
└── common.jar

Séparez les paramètres de chaque projet du projet racine, ce qui facilite l'ajout de nouveaux projets

À ce stade, vous pouvez faire ce que vous vouliez faire pour le moment. Ensuite, je voudrais ajouter un projet app2 pour le rapprocher d'un exemple réel, mais je vais d'abord organiser le script.

Séparez les paramètres d'application uniquement dans app / build.gradle

Je voulais le déplacer pour le moment, j'ai donc répertorié les paramètres du projet d'application dans build.gradle du projet principal. Cependant, si rien n'est fait, le nombre de descriptions similaires à master / build.gradle augmentera à mesure que le nombre de sous-projets augmente.

Même si j'augmente le nombre de sous-projets, je souhaite conserver les modifications uniquement dans settings.gradle. Par conséquent, les paramètres du projet d'application sont séparés en build.gradle dans le répertoire de l'application:

master$ tree ../ -L 2
../
├── app
│   ├── build.gradle
│   └── src
├── common
│   └── src
└── master
    ├── build.gradle
    └── settings.gradle

Cas d'échec

À première vue, cela semble réussir, mais cela échoue.

Pour app / build.gradle, spécifiez uniquement la classe principale qui est différente pour chaque projet. Dans master / build.gradle, déplacez ce qui a été écrit dans le bloc project (': app') vers le bloc subprojects:

app/build.gradle


jar {
    manifest {
        attributes 'Main-Class': 'com.github.kazuma1989.dec10_2016.app.Main'
    }
}

master/build.gradle


…

subprojects {
    …

    jar {
        manifest {
            attributes 'Class-Path': configurations.runtime.collect{it.name}.join(' ')
        }

        doLast {
            copy {
                from configurations.runtime
                into libsDir
            }
        }
    }
}

// project(':app') {
// }

Une erreur se produit à la ligne 33 (où ʻattribute'Class-Path'`):

master$ gradle clean jar

FAILURE: Build failed with an exception.

* Where:
Build file '/Users/kazuma/qiita/2016/dec10/master/build.gradle' line: 33

* What went wrong:
A problem occurred evaluating root project 'master'.
> Could not resolve all dependencies for configuration ':app:runtime'.
   > Project :app declares a dependency from configuration 'compile' to configuration 'default' which is not declared in the descriptor for project :common.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 0.837 secs

Il semble qu'une erreur se soit produite car le configurations.runtime utilisé dans l'application n'a pas pu être résolu. En effet, les paramètres du projet d'application ont été exécutés avant d'appeler le plug-in Java dans le projet commun.

groovy:master/build.gradle(app/build.(Avant la séparation des grades)


subprojects {
    // app ->Le projet est défini dans l'ordre du commun (suite ci-dessous)
    apply {
        plugin 'java'
        …
    }

    …
}

project(':app') {
    //Ce bloc a été exécuté après l'application du plug-in Java à common, il n'y a donc pas eu de problème.
    jar {
        …
    }
}

groovy:master/build.gradle(app/build.Après la séparation des grades)


subprojects {
    // app ->Le projet est défini dans l'ordre du commun.
    apply {
        plugin 'java'
        …
    }

    // project(':app')Étant donné que le traitement qui était en cours a été introduit dans le bloc des sous-projets, il sera exécuté avant d'appliquer le plug-in Java à commun, et une erreur se produira.
    jar {
        …
    }
}

Cas réussi

Cela fonctionne bien si vous divisez le bloc sous-projets en" tous les sous-projets, y compris les sous-projets communs et les applications "et" sous-projets autres que communs ". La signification du bloc est également claire, c'est donc également meilleur pour la lisibilité:

master/build.gradle


//Tous les sous-projets, y compris les projets communs et les applications
subprojects {
    apply {
        plugin 'java'
        …
    }

    …
}

//Sous-projets autres que courants
subprojects {
    if (project.name in ['common']) return

    dependencies {
        …
    }

    jar {
        …
    }
}
master$ gradle clean jar
:app:clean
:common:clean
:common:compileJava
:common:processResources UP-TO-DATE
:common:classes
:common:jar
:app:compileJava
:app:processResources UP-TO-DATE
:app:classes
:app:jar

BUILD SUCCESSFUL

Total time: 0.94 secs

master$ java -jar ../app/build/libs/app.jar 
common

Remplacez app / build.gradle par gradle.properties

Remplacez app / build.gradle par gradle.properties car les paramètres de chaque projet ne diffèrent que par leur valeur. Comme build.gradle, gradle.properties est un fichier de propriétés qui est automatiquement chargé lorsqu'il existe directement sous le répertoire du projet:

master$ tree ../ -L 2
../
├── app
│   ├── gradle.properties
│   └── src
├── common
│   └── src
└── master
    ├── build.gradle
    └── settings.gradle

app/gradle.properties


jar.manifest.attributes.Main-Class=com.github.kazuma1989.dec10_2016.app.Main

Depuis build.gradle, récupérez la valeur avec la méthode getProperty.

La mise en garde est que vous devez utiliser le bloc ʻafterEvaluate. En effet, les projets sont chargés dans l'ordre du projet principal et du projet d'application, donc app / gradle.properties n'a pas encore été chargé au moment de master / build.gradle. Les blocs ʻAfterEvaluate fonctionnent après que chaque projet est évalué (après avoir terminé la configuration du projet), donc cela fonctionne:

master/build.gradle


…

subprojects {
    …

    afterEvaluate {
        jar {
            manifest {
                attributes 'Main-Class': getProperty('jar.manifest.attributes.Main-Class')
                attributes 'Class-Path': configurations.runtime.collect{it.name}.join(' ')
            }

            …
        }
    }
}

Résumé

Au final, la configuration multi-projets ressemble à ceci:

master$ tree ../ -L 2
../
├── app
│   ├── gradle.properties   //Écrivez les paramètres pour chaque projet
│   └── src                 //Code source pour chaque application. Dépend du commun
├── common
│   └── src                 //Parties communes
└── master
    ├── build.gradle        //Définir les tâches utilisées dans tous les projets
    └── settings.gradle     //Spécifier les répertoires à inclure dans le multi-projet

Tous les JAR sont générés en tapant «gradle jar» dans le répertoire principal. Vous pouvez générer un JAR séparé avec gradle jar ou gradle: common: jar dans chaque répertoire de projet. Le JAR généré est organisé comme un JAR qui dépend du répertoire libs, vous pouvez donc l'utiliser tel quel si vous transportez le répertoire entier.

Si nous augmentons le nombre de projets dans le futur, ce sera comme suit:

master$ tree ../ -L 2
../
├── app
│   ├── gradle.properties
│   └── src
├── app2
│   ├── gradle.properties
│   └── src
├── app3
│   ├── build.gradle        //Des scripts peuvent être placés si des paramètres spéciaux sont requis
│   ├── gradle.properties
│   └── src
├── common
│   ├── build.gradle        //Spécifiez les bibliothèques qui dépendent de
│   └── src
└── master
    ├── build.gradle
    └── settings.gradle

c'est tout.

Recommended Posts

Créer un JAR exécutable multi-projets avec Gradle
Multi-projets Spring Boot 2 avec Gradle
Configurer un multi-projet Gradle dans IntelliJ pour créer un fichier JAR
Générer un seul fichier jar exécutable avec des bibliothèques dépendantes dans Gradle 4.9
Créer un fichier exécutable avec Android Studio
Configurer un multi-projet avec des sous-répertoires dans Gradle
Comment créer un fichier exécutable dans Maven
Faites un blackjack avec Java
Notes de puzzle à Gradle
Mémo des paramètres Gradle (multi-projets pour tout en un) pour moi-même
Créez un fichier jar qui peut être exécuté sur Gradle
Créer un multi-projet Java avec Gradle
Pot Gradle + Kotlin-Generate avec DSL
Copier les pots dépendants dans Gradle 5
Refactoring: faire du Blackjack en Java
Tâches de contrôle effectuées par Gradle
[Gradle] L'histoire selon laquelle le fichier de classe n'existait pas dans le fichier jar
Comment créer un fichier jar sans dépendances dans Maven
Comment créer un nouveau projet Gradle + Java + Jar dans Intellij 2016.03
Ajouter un horodatage au nom de fichier JAR dans Gradle