A reminder of the steps to manage a multi-project using the build tool Gradle and create an executable JAR as an artifact.
--Multi-project management --Creating an executable JAR that depends on an external JAR (JAR that includes all dependent JARs, not fat-JAR)
Is the main theme.
I had the opportunity to create a GUI application with JavaFX [^ JavaFX]. The application is finally distributed in EXE format by a tool called javapackager [^ javapackager], but as a preliminary step, it was necessary to generate a JAR that can be started by double-clicking.
[^ JavaFX]: 1 JavaFX Overview (Release 8) (https://docs.oracle.com/javase/jp/8/javafx/get-started-tutorial/jfx-overview.htm) [^ javapackager]: Self-contained application packaging (https://docs.oracle.com/javase/jp/8/docs/technotes/guides/deploy/self-contained-packaging.html)
In addition, applications had to create multiple types with almost the same internal processing and different detailed behavior. I wanted to make the common part common.jar and the JAR as the entrance to app1.jar, app2.jar (each depends on common.jar), but I looked into Jenkins in the company and searched for a multi-project with a similar configuration. Even when I tried it, there was no script that was completed only with Gradle. There was only a shell script that called the common build.gradle and then the app1 build.gradle.
I could have achieved the purpose by imitating the shell script, but for learning, I decided to create a script that is completed in Gradle.
For simplicity, let's start with just one app project.
First, create a minimal multi-project and call the hello
task to check the operation.
Create directories app, common, master directly under the working directory, and create build.gradle and settings.gradle directly under master [^ tree command on Mac]:
[^ Tree command on Mac]: Tree command on Mac --Qiita (http://qiita.com/kanuma1984/items/c158162adfeb6b217973)
master$ tree ../
../
├── app
├── common
└── master
├── build.gradle
└── settings.gradle
In settings.gradle, specify the directory you want to recognize as a subproject. The root project is not specified, but the directory named master is automatically recognized as the root project, so it is not specified [^ Multi-project with Gradle]:
[^ Multi-project with Gradle]: Multi-project with Gradle-Qiita (http://qiita.com/shiena/items/371fe817c8fb6be2bb1e)
settings.gradle
includeFlat 'app', 'common'
In build.gradle, define a hello
task inside the ʻall projects` block [Why use ^ doLast]:
[Reason for using ^ doLast]: The leftShift
method and the<<
operator have been deprecated. (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}."
}
}
}
At this point, call the hello
task in the master directory [meaning ^ q].
If you see the following, it's OK:
[Opinion of ^ q]: The -q
option is used to make the result of println
easier to see.
master$ gradle -q hello
I`m master.
I`m app.
I`m common.
By the way, if you call the hello
task in the common directory, only the common task will be called.
You can also use this property to build only specific projects:
common$ gradle -q hello
I`m common.
Add the Eclipse plug-in to your subproject. Add the following after the ʻall projects` block:
build.gradle
…
subprojects {
apply {
plugin 'eclipse'
}
}
With this alone, you can use the ʻeclipse` task and create an Eclipse project:
master$ gradle eclipse
:app:eclipseProject
:app:eclipse
:common:eclipseProject
:common:eclipse
BUILD SUCCESSFUL
Total time: 2.067 secs
However, I actually need the src directory, so I will generate it with the gradle task [reason for not using the ^ init task]. Add a Java plugin and create a ʻinitSrcDirs` task as shown below.
[Reason for not using ^ init task]: Gradle comes with a ʻinit` task, but I didn't want to create extra files, so I defined it myself.
build.gradle
…
subprojects {
apply {
plugin 'eclipse'
plugin 'java'
}
task initSrcDirs {
doLast {
sourceSets.all {
java.srcDirs*.mkdirs()
resources.srcDirs*.mkdirs()
}
}
}
}
ʻWhen you run the initSrcDirs` task, src / main / java, src / main / resources, etc. are created. Even if the files already exist in those paths, they will never be empty, so it's safe to use:
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` Describes the variables that appeared when defining the task.
--sourceSets
is a directory system such as src / main / java defined by Java plugins.
--mkdirs ()
calls the mkdirs method of java.io.File because srcDirs
is a list of java.io.File.
If you're curious about other syntax (for example, iteration * .
), check out Groovy's grammar.
If you call the ʻeclipse task again with the src directory created, the classpath will also pass properly (the ʻeclipse Claspath
task that was not there earlier is called):
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
Let's get down to the essence of what we wanted to achieve with multi-project management. Make the app dependent on common.
First, in order to reproduce the failure case, create the following 2 classes without changing build.gradle. Constant.java is created under common, and Main.java is created under app. Of course, even Eclipse doesn't recognize each other's dependencies, so it can't be complemented and an error occurs:
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);
}
}
If you run the jar
task in this state, the build will fail as expected.
The compileJava
task, which precedes the jar
task, has failed:
master$ gradle jar
:app:compileJava
/Users/kazuma/qiita/2016/dec10/app/src/main/java/com/github/kazuma1989/dec10_2016/app/Main.java:3:error:Package com.github.kazuma1989.dec10_2016.common does not exist
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:error:Can't find symbol
System.out.println(Constant.NAME);
^
symbol:Variable constant
place:Class Main
2 errors
: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
Then make the build successful.
Add the following to the end of the subprojects
block to define the dependency on common:
build.gradle
…
subprojects {
…
//Settings other than common
if (project.name in ['common']) return
dependencies {
compile project(':common')
}
}
This time the jar
task succeeds and the JAR I created works fine.
JARization of common is executed first, not in ascending order of project names:
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
The jar
task does not necessarily have to be called from the master, only the one in the app can be called.
Even in that case, the app will be JARed after compiling common, so it will succeed properly:
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
After setting the dependency, if you execute the ʻeclipse` task again, the error in Eclipse will be resolved and you will be able to use code completion.
Set the manifest for the jar
task and generate an executable JAR.
Just add a minimal statement at the end of build.gradle and try running the jar
task anyway:
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
When you call the created JAR with the java
command, the Main class is called:
master$ java -jar ../app/build/libs/app.jar
common
But it just happened to work. This is because the Constant class cannot be found unless the dependency information for common.jar is described in the manifest. This time, the Main class only references Constant string constants, so it's possible that the value was retained in the Main class at compile time.
Normally, it will be NoClassDefFoundError. It means that the compilation succeeds, but fails at run time.
For example, the following is the case where the base class ʻAbstractApplication` is created in common and inherited by 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
To solve this, change the inside of the jar
block as follows.
ʻAttributes'Class-Path'has been added to the manifest, and a
copy` operation has been added to the task:
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
contains information about the runtime dependencies of the app project.
It also contains information about the common project specified as compile
in the dependencies
block.
This is because compilation dependencies are also runtime dependencies.
In ʻattributes'Class-Path', the contents of
configurations.runtimeare concatenated with spaces. So the contents of the manifest will be
Class-Path: common.jar (if you have added dependencies other than common, it will be
Class-Path: common.jar another.jar extra.jar`. ).
The JAR that says Class-Path: common.jar
in the manifest will look for common.jar in the same directory as itself, so copy common.jar to the same directory as app.jar in the copy
block. I am.
By adding the manifest and copying the dependent JAR, the JAR execution is now successful. Carry the libs directory and your application is complete:
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
At this point, you can do what you wanted to do for the time being. Next, I would like to add an app2 project to bring it closer to a real-life example, but first I will organize the script.
I wanted to move it for the time being, so I described the settings of the app project in the build.gradle of the master project. However, if nothing is done, the number of descriptions similar to master / build.gradle will increase as the number of subprojects increases.
Even if I increase the number of subprojects, I want to keep the changes only in settings.gradle. Therefore, the app project settings are separated into build.gradle in the app directory:
master$ tree ../ -L 2
../
├── app
│ ├── build.gradle
│ └── src
├── common
│ └── src
└── master
├── build.gradle
└── settings.gradle
At first glance it seems to succeed, but it fails.
For app / build.gradle, specify only the main class that is different for each project.
In master / build.gradle, move what was written in the project (': app')
block to the subprojects
block:
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') {
// }
An error occurs on line 33 (where ʻattributes'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
It seems that an error occurred because the configurations.runtime
used in the app could not be resolved.
This is because the settings of the app project were executed before calling the Java plugin in the common project.
groovy:master/build.gradle(app/build.(Before gradle separation)
subprojects {
// app ->The project is set in the order of common (continued below)
apply {
plugin 'java'
…
}
…
}
project(':app') {
//This block is executed after the Java plugin is applied to common, so there was no problem.
jar {
…
}
}
groovy:master/build.gradle(app/build.After gradle separation)
subprojects {
// app ->The project is set in the order of common.
apply {
plugin 'java'
…
}
// project(':app')Since the processing that was in was brought to the subprojects block, it will be executed before applying the Java plug-in to common, and an error will occur.
jar {
…
}
}
Breaking the subprojects
block into" all subprojects, including common and app "and" non-common subprojects "will work.
The meaning of the block is also clear, so this is also better for readability:
master/build.gradle
//All subprojects including common and app
subprojects {
apply {
plugin 'java'
…
}
…
}
//Subprojects other than common
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
Since the settings for each project differ only in the value, I replace app / build.gradle with gradle.properties. Like build.gradle, gradle.properties is a property file that is automatically loaded when it exists directly under the project directory:
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
From build.gradle, get the value with the getProperty
method.
The caveat is that you need to use the ʻafterEvaluate block. This is because the project is loaded in the order of master project and app project, so app / gradle.properties has not been loaded yet at the time of master / build.gradle. ʻAfterEvaluate
blocks work after each project has been evaluated (after completing the project setup), so it works:
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(' ')
}
…
}
}
}
In the end, the multi-project configuration looks like this:
master$ tree ../ -L 2
../
├── app
│ ├── gradle.properties //Write the settings for each project
│ └── src //Source code for each application. Depends on common
├── common
│ └── src //Common parts
└── master
├── build.gradle //Define tasks to be used in all projects
└── settings.gradle //Specify directories to include in multi-project
All JARs will be generated by typing gradle jar
in the master directory.
You can generate a separate JAR with gradle jar
in each project directory or gradle: common: jar
.
The generated JAR is organized as a JAR that depends on the libs directory, so you can use it as it is if you carry the entire directory.
If we increase the number of projects in the future, it will be as follows:
master$ tree ../ -L 2
../
├── app
│ ├── gradle.properties
│ └── src
├── app2
│ ├── gradle.properties
│ └── src
├── app3
│ ├── build.gradle //Scripts may be placed if special settings are required
│ ├── gradle.properties
│ └── src
├── common
│ ├── build.gradle //Specify the library that common depends on
│ └── src
└── master
├── build.gradle
└── settings.gradle
that's all.
Recommended Posts