Gradle is considered to be a build script language. Maven hides the build logics inside plugins, so it would be necessary to build up an own plugin in order to generate the particular build logics.
The build script is an actual programming language, and not a series of XML tags as happens in Maven.
The used programming language is Groovy and, since 2016, Kotlin stepped in.
Both are compatible with JVM, so they interact well with Java.
Gradle is usable also in many other projects such as Android, C, C++, ecc..
Define a variable
def courses = [“neoj4”, “maven-101”, “maven-102”, …]
Iterate a collection
courses.each {
println it
}
The it
variable is a reserved variable that represents each item of the collection.
Groovy is much more concise than Java in the syntax.
Class definition:
class Person {
String name
Integer age
Person(name, age) {
this.name = name
this.age = age
}
def increaseAge(Integer years) {
this.age += years
}
}
The Java code can be included in the project definition Gradle file: build.gradle. In the case of a project with more subprojects it would be necessary to define the settings.gradle file.
Example of a build.gradle for a Java project:
apply plugin: ‘java’
defaultTasks ‘clean’, ‘compileJava’
repositories {
mavenCentral()
}
dependencies {
compile ‘junit:junit:3.8.1’
}
The managing of dependencies is very similar to Maven. Concerning dependencies and repositories, Gradle and Maven have many concepts in common.
Java JDK installation is required along with an Ide to manage and configure Gradle (it can also be managed by terminal).
To verify the right Java installation and the correct configuration of the JAVA_HOME
environment variable and the consequent valorization of the system variable PATH
with %JAVA_HOME%/bin
, execute the following Powershell command:
$env:java_home
which should return the JDK directory. So:
java -version
should return the installed Java version.
I task sono poi eseguibili dall’Ide tramite i rispettivi plugin (per esempio Gradle per IntelliJ). Nel build.gradle:
defaultTasks ‘hello’
task (‘hello’).doLast {
println “Hello World”
}
IntelliJ dovrebbe riconoscere Gradle in automatico, inclusivo dei suoi task e le loro esecuzioni.
Per via della compatibilità tra Java e Groovy al 99% consegue che
println “Hello World”
può essere scritto
System.out.println “Hello World”
Il che consente, ad esempio, di utilizzare le classi di date di Java, sempre tramite importazione:
import java.text.*
SimpleDateFormat sdf = new SimpleDateFormat(“format”)
println “Hello world now:” + sdf.format(new Date())
Esempio di inline code:
void sayHelloWorld() {
println “Hello world!”
}
sayHelloWorld()
Return is an optional operator in Groovy methods. The value of the last sentence is returned.
int doubleInt(int i) {
println(i)
i * 3 ← is returned
}
prinln doubleInt(2)
It applies also in the case:
int doubleInt(int i) {
println(i)
i * 3
def y = i ← is returned
}
Groovy is optionally typed, that means it uses dynamic typing. So the return of the methods and variables can be non typed.
Variables are declared with the def
operator, however they can be declared with the type of the variable, for example instead of
def y = i
it can be used
Integer y = i
even if, in this case, the value assigned must correspond. So, despite dynamic typing, the language is characterized by type check.
Strings can be written both double or single quoted. Groovy supports even multiline strings, by using three double quotes: “””
.
It supports also string interpolation:
def x = 4
println “x is $x”
as in the String.format()
of Java or similar. Variables can be included inside the string formatted inline
def myCourse = “gradle”
println “I’m training in: ${myCourse.toUpperCase()}”
String interpolation works only with double quotes, otherwise the string is evaluated literal.
Groovy doesn’t require getters and setters to define properties.
The following class has two instance variables:
class Person {
String name
Integer age
Person(name, age) {
this.name = name
this.age = age
}
}
it is possible to create an instance of this class and set or get the related property:
def p1 = new Person(“Fred”, 35)
prinln p1.age
will return
35
and
p1.age = 36
Getting and setting on a hash map is done using the keys which represent properties in Groovy. So:
Map m = new HashMap()
m.put(“foo”, “Fred”)
m.get(“foo”)
can be
m.foo = “bill”
println m.foo
Key concept to understand Groovy and Gradle.
Closures are defined as follows:
def echoIt = { }
and get invoked like methods:
prinln echoIt()
Closures can be thought of as anonymous inner classes in Java. As in methods, closures return the value of the last statement.
def echoIt = {
prinln “Hello World”
}
will return null but println is executed.
Parameters:
def echoIt = { parameter ->
println parameter
}
echoIt(“Hello World”)
optionally
def echoIt = { String parameter ->
println parameter
}
echoIt(“Hello World”)
The untight it parameter (as seen in the each operator) can be used as well to not define parameters.
There can be more parameters separated by comma.
Closure is a specific type:
Closure echoIt = { a, b, c ->
println a
println b
println c
4 ← returned
}
so Closures can be passed to methods as parameter:
def oneArgMethod(closure) {
closure() * 2
}
println oneArgMethod{10}
will return “8”.
def twoArgMethod(factor, closure) {
closure() * factor
}
println twoArgMethod 3, {10}
This pattern is used frequently in Gradle (è una cosa banale in Javascript o Java 8).
def i = oneArgMethod {
def y = 3
y * 2
}
Common use cases:
// instead of
for (int j in [1, 2, 3]) {
println j
}
// we have
[1, 2, 3].each {
println it
}
A closure is “closed” over a context.
class Person {
String name = “Fred”
Closure nameSayer = {
println name
}
}
def p1 = new Person()
p1.nameSayer()
def theName = “John”
def sayName = {
println theName
}
sayName()
If the contents of the variables change the values of the Closure result. The Closures keep a reference to the context object in which they are defined and not a reference to the values. JavaScript also uses this concept a lot, and it’s really key in actually understanding how Gradle uses Groovy as a scripting language too. The context, in Groovy, is known as owner object or delegate object.
class Person {
String name
Person(name) {
this.name = name
}
def executeInside(Closure c) {
c.delegate = this
c.()
}
}
def p1 = new Person(“Fred”, 35)
p1.executeInside{println name}
With the delegate we can execute an arbitrary block of code against that object and also have access to the instance variables of that object. We can add arbitrary code to a class without having to actually change the class.
Cornerstone of the great Domain Specific Language (DSL) of Gradle.
6 key
When we write a .gradle file we are actually accessing an object that implements the Script interface. That exposes many properties and methods, visible from documentation.
For example, the logger property:
logger.info “Hello!”
The Script interface also supplies us the apply method that takes in a Closure. We can use it through the code block pattern seen before. By using apply methods we are asking Gradle to actually apply a Closure.
apply {
println “Hello again!”
}
The initialization phase maps the init.gradle files and optionally the settings.gradle. It can also have other files in a specific folder ending with .gradle. Allows to set up different configurations, like properties for developer environment against production environment (database connection credentials, register billed listeners and also register build logger’s).
The settings.gradle file is more specific and oriented towards multi project projects. This fase relise on the build.gradle file and the execution phase also relies on the build.gradle. In the case of a multi project we have multiple build.gradle files.
The initialization files live in a special folder called .gradle/initi.d. The path .gradle/ is the Gradle home directory. They are evaluated following the filename order, so the evaluation order might be: another.gradle, init.gradle, zingAnother.gradle, settings.gradle. Then the Configuration phase evaluates build.gradle.
The initialization allows us to set up our built environment before we actually configure and execute the build itself.
In the example below a Closure is actually set onto a property called Timestamp
of the Gradle delegate object:
import java.text.*
gradle.ext.timestamp = {
def df = new SimpleDateFormat(“..”)
df.setTimeZione(TimeZone.getTimeZone(“UTC”))
return df.format(new Date())
}
When we set a property in the initialization script we can have access to it throughout the entire build lifecycle.
The property on the delegate is like a global variable, we can access it in each of the build scripts. Because that property contains a Closure, I can execute that Closure.
gradle.timestamp()
gradle
is an object accessible along the entire build lifecycle.
Every project has a build.gradle file that represents a script (implements Script<interface>
). Through this we have access to several properties and methods (such logger and apply), but also to the delegate object, which can change depending on the part of the build lifecycle. The delegate object, in the configuration phase, implements the Project interface, so it makes available a series of properties and methods as well.
The settings.gradle file is mandatory when we are creating a multi-projects project. Here too we have access to the Script Objects or an object that implements the Script interface. But we have access to a different delegate object. In this phase it implements the Settings interface, giving us a different bunch of properties and methods.
To use the initialization script, we’ll use the init.gradle file. So we have access to the same Script interface and also to a delegate object that comes along with it. This time the delegate object implements the Gradle interface (at a higher level). This type of script is evaluated in the initialization phase, and at this point Gradle doesn’t know anything about any project in the build.
Script | Phase | Delagates to object |
---|---|---|
build.gradle | Configuration | Project interface |
settings.gradle | Initialization | Settings interface |
init.gradle | Initialization | Gradle interface |
Both Project interface
and Settings interface
have acces to Gradle interface methods and properties.
https://docs.gradle.org/current/javadoc/org/gradle/api/invocation/Gradle.html
To invoke the Closure we defined on the project delegate we can use both project.gradle.timestamp()
and gradle.timestamp()
.
The properties and methods exposed by the Gradle object are consultable from the documentation. For example
logger.info “>>>> build.gradle: ${project.gradle.gradleVersion}”
logger.info “>>> build.gradle: gradle.gradleHomeDir” (more concise)
In the init Phase the gradle object is implied, so we can call the property directly
logger.info “>>> init build.gradle: $gradleVersion”
The Gradle object (available according to documentation from getGradle() -> Gradle) implements the Gradle interface,
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html The Project object is the default object or delegate object within the build scripts. The build scripts live in a buid.gradle file. So it is the context of every build script.
The method of the Project object getBuildFile()
contains all the information about the file we’re actually using at the moment.
logger.info “>>> build.gradle: $project.buildFile”
project.relativePath(project.buildFile)
relativePath will return the relative path of the file.
Plugins add new tasks extending project capatibilities. They extend Gradle object.
Adding the Java plugin:
plugins {
id 'java'
}
Applying the Java plugin provides following tasks:
The jar task depends on testClasses which depends on compileJava. The Gradle documentation shows what task are available.
By convention, Java plugin structure and project structure assumes the same project layout structure of Maven. For example the Java sources lives in a directory src/main/java
, the resources in src/main/resources
and the tests in src/test/java
.
The Java plugin also provides a number of lifecycle tasks. When the build run for example, performs a full build of the project and depends on check and assemble. check performs verification tasks for the project and assemble assemblies all the archives in the project. Another usefull task, that is not part of lifecylce tasks, is clean. It actually deletes the project directory. It's recomended to execute a clean before build a project.
The project could be initializated by the IDE using specific Gradle addons.
Create the build.gradle file at the project root.
Import the desired plugins:
plugins {
id 'java'
}
In the documentation is available a complete description of source sets and tasks of the Java plugin. By applying the Java plugin many task are imported and visible from the IntelliJ/Eclipse Gradle panel to be executed. By creating a new configuration of Gradle we can tell which tasks execute (for example starting with clean). But only applying Java plugin is not enough, cause the build will raise dependency error and will be needed to "import" those dependencies.
A repository is a mechanism to includes all jars or artifacts needed to compile aginst. A repository is a bunch of server in the internet that hold or cache all the artifact needed to build a Java application. The most popular is Maven Central.
By defining a Clousre in the repsoitories context, the delegate object changes so we have a different context. In the repository context we have access to a whole bunch of different methods, including mavenCentral.
plugins {
id 'java'
}
repositories {
mavenCentral()
}
That is the same of saing:
repositories {
delegate.mavenCentral()
}
In the repositories block the actual delegate object is RepositoryHandler
so we are actually using an instance of RepositoryHandler
. It provides several methods including mavenCentral()
.
But this is not enough to have access to the dependency we need. We also have to tell Gradle the actual artifact we want to download as a dependency. We do so by using the dependencies { }
block. This works in the exact same way as the repositories block. So the delegate object is a DependencyHandler on which we have access.
dependencies {
implementation
}
The build Maven tool have coordinates to identify the dependency. So in Gradle is the same. To locate the jar file or the artifact is usefull to navigate http://www.mvnrepository.com/. For this example we are going to use th Apache Commons Math artifact, choosing the last version. We already know how mvnrepository works. In the case the repository suggest to use compile
keyword: it's deprected, we have to use the implementation keyword. The same happens for the keyword testCompile
(now is testImplementation
.)
dependencies {
implementation group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'
}
also short:
dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
}
And the test implementation for JUnit:
dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
testImplementation 'junit:junit:4.12'
}
No we can run build
and then clean
.
The properties live in the API Objects and are accessible through getters or setters. Some properties live in the interfaces, for example the Logger object lives in the Script<Interface>
.
Gradle allows “key-value” pairs in a “gradle.properties” file. Available to scripts in the settings.gradle and build.gradle files.
gradlePropertiesProp=gradlePropertiesValue
The gradle.properties file can live in the root of the project or in the GRADLE_USER_HOME
directory (.gradle folder) that can be defined with -Dgradle.user.home
command line.
It is also possible to create our own properties that are added to the Gradle API, for example a Closure.
Accessing a property that doesn’t exists cause a runtime exception, so we can use the hasProperty()
method:
project.hasProperty(‘gradlePropertiesProp’)
or
hasProperty(‘gradlePropertiesProp’)
Another way to set custom property is to pass them in the command line options:
-PcommandLineProjectProp=commandLineProjectValue
The extra properties extension allows new properties to be added to existing domain objects. See ExtraPropertiesExtension
documentation page.
All domain objects that implement an interface called ExtensionAware
intrinsically have access to extended name properties.
project.ext.sayHello = “Hello!!”
logger.info sayHello
Defined dependencies in the build.gradle file are Direct Dependencies.
These dependencies can also have dependencies on other artifacts or jar files. These other files or artifacts are known as transitive dependencies, the dependencies of our direct dependencies.
Using external third party dpendencies can causes conflicts. For example Springframework Webmvc has common transitive dependency with Springframework Core (Apache commons-logging).
How to discover transient dependencies?
dependencies {
implementation 'org.springframework:spring-webmvc:4.0.3.RELEASE'
implementation 'log4j:log4j:1.2.17'
... other implementation dependencies ...
compileOnly "javax.servlet:javax.servlet-api:3.0.1"
testImplementation "junit:junit:4.11"
}
By running the task dependencies
Gradle logs out all the dependencies tree. The transitive dependencies are shown and, probably, we can see common nested dependencies that are repeated along the tree (the logging library for example).
We can use adapter such as SLF4J (Simple Logging Facade for Java) that wraps the logging library to avoid potential conflicts that might arise by having lots of different frameworks being used for performing logging.
So we can go on with including SLF4J Artifact or Jar file in our project. The other thing we also need to remove dependency for Jakarta logging framework that comes with Spring Web MVC and we want to use the SLF4J adapted for that.
We found out the coordinates of the dependency in Maven Central : org.slf4j SLF4J LOG4J 12 Binding, this can save as from refactoring code on which logging in used because it works like a facade interface.
Adding the dependency to the dependencies
task:
implementation 'org.slf4j:slf4j-log4j12:1.7.25'
So we can remove the direct dependency by removing:
implementation 'log4j:log4j:1.2.17'
Now by performing a clean build and running dependencies
task we'll see that the logging dependency still occur transitive among Spring dependency.
So, we want everything to go through the SLF4J facade rather than using the Apache Commons library.
We will add the library JCL 1.2 Implemented over SLF4J.
implementation 'org.slf4j:jcl-over-slf4j:1.7.25'
Then we will exclude the transient dependency from Spring WebMVC by using the exclude
function:
implementation( 'org.springframework:spring-webmvc:4.0.3.RELEASE') {
exclude group 'commons-logging', module: 'commons-logging'
}
By running the dependencies task we will se that the transient library has been removed. Spring will work anywhay because will uses jcl-over-slf4j.
Using the Project Report Plugin for a further dependencies analisys.:
apply plgin: 'project-report'
This provides a series of tasks (see documentation) such as htmlDependencyReport
.
By running this task it will produce a folder reports
, inside the build directory, on which a nicely formatted HTML document is created (index.html).
By running dependencyReport
task it will produce a txt file with the same content.
A Gradle project (Project<Interface>
) (that could be a deployment, a jar build, a dependency or an entire web application build) is made up of a collection of tasks (Task<Interface>
).
A Task represents an atomic piece of work performed by the build.
Configuration Phase: Create Tasks Configure Tasks
Execution Phase Execute Tasks
A Task can be made up of zero or many Actions (Action<Interface>
).
The collection of tasks is held in the project by the Task Container.
The Task Interface provides some helpers. The most useful are: doFirst(Action or Closure)
and doLast(Action or Closure)
. They return the Task itself, allowing chain actions.
Any tasks defined in the build.gradle are unique in the project.
task hi
project.hi.doLast {
// code
}
description and group are other properties of the Task Interface.
task hello {
description = “Log the name of the Task”
group = “Welcome”
doFirst {
logger.info “”My name is $name and this is my 1st defined Action”
}
doLast {
logger.info “”My name is $name and this is my 2nd defined Action”
}
}
logger.info hello.description
logger.info hello.group
Adding defaultTask ‘hello
’ on the top of the build.gradle file makes the hello task being executed without specifying it from the command line parameters. I can also specify a list of tasks by using the method defaultTasks ‘A’, ‘B’, ‘C’
By doing
hello.doLast {
logger.info “My name is: $name and this is my 3rd defined Action”
}
Gradle will add this line into the sequence of last actions to be executed. So we will have a doFirst and two doLast actions. If I define another doFirst()
action it will be executed first, like it’s overriding the order of the execution. The same happens, reversed, in the doLast()
.
Another way of defining in chain actions:
hello.doFirst {
// code
}.doLast {
// code
}.doFirst {
// code
}
We can specify that a task depends on another task that must be executed before itself being executed by using the keywor dependsOn
.
defaultTasks 'doStartProcess', 'doStep2'
task doStartProcess {
doLast {
logger.info "$name starting process"
}
}
task doStep2 (dependsOn: 'doStartProcess') {
doLast {
logger.info "$name Performed OK"
}
}
so doStep2()
will be executed once doStartProcess()
has executed first. It defines a sort of order of execution independent from the order pointed out in the defaultTasks. dependsOn accepts also a list:
task doStep2 (dependsOn: ['doStartProcess', 'anotherTaskInTheWall']) {
doLast {
logger.info "$name Performed OK"
}
}
Gradle builds up a graph of the dependencies. If more than one task depends on the same task, then it would not be executed twice, but simply the tasks depending on it will be executed cause Gradle knows that that task on which they depend has been already executed.
We can access the tasks through the TaskContainer
interface:
logger.info ">>> ${project.tasks.findAll { task -> task.name.startsWith('doStep2') } }"
This method on which we passed a Closure filtering by name will return all the tasks whose name starts with "doStep2
". The returning list can be used within the dependsOn parameter:
task doSomethingInTheMiddle(dependsOn: ['doStartProcess', project.tasks.findAll { task -> task.name.startsWith('doStep2') }] ) {
…
}
We can set dependsOn as a task property because the task, once defined, is part of the Gradle global object.
task doSomethingInTheMiddle {
…
}
doSomethingInTheMiddle.dependsOn doStartProcess, doStep2, tasks.findAll { task -> task.name.startsWith('doStep2') }] )
This allows us to have more dynamic dependencies, because depending on the build environment we can have different dependencies, so this dependsOn
setting can be done inside a conditional statement:
if (condition) {
task.dependsOn task1, task2
} else {
task.dependsOn task3, task4
}
https://en.wikipedia.org/wiki/Directed_acyclic_graph
Cricular dependencies can cause a RuntimeException.
We do have access to a callback method whenReady(Closure)
. This method leaves on the graph and it executes the Clousure that gets provided to when the graph has actually been built and is ready to be executed. With some other callback methods we can interrogate and access the graph:
beforeTask()
: gets called back before a task actually runsafterTask()
: gets called after the task actually has runFor access the Gradle graph:
logger.info ">>> $project.gradle.taskGraph"
an instance of DefaultTaskGraphExecutor
that is an implementation of the TaskExecutionGraph
interface. This interface provides a lot of methods including getAllTasks()
.
logger.info ">>> $project.gradle.taskGraph.allTasks"
but if we invoke this method during the configuration phase we will have a failure. So we will use whenReady()
. So:
project.gradle.taskGraph.whenReady {
logger.info ">>> $project.gradle.taskGraph.allTasks"
}
it will log a collection of tasks that made up the graph.
This is useful when, in the build process phase, we need to do some conditional logic based on the dependent tasks. For example, we want to set the version which accesses a property on the project object, but we want to set the version number through a different one, depending on a particular task existing or not. We can do this using the interface method hasGraph()
.
if (taskGraph.hasTask(doStep2)) {
project.version = '1.0'
} else {
project.version = '1.0-SNAPSHOT'
}
project.gradle.taskGraph.beforeTask {task ->
… access to the task
}
and through this interface we can access the actual task with a Closure or an Action:
project.gradle.taskGraph.beforeTask {task ->
… access to the task
}
project.gradle.taskGraph.afterTask {task ->
… access to the task
}
Execute the clean
and build
taks of the Java plugin. "build" is calling the compileJava
task. The IDE plugin show the all lifecycle and the elapsing time of each task.
The task jar
, of the Java plugin, actually build a JAR file that contains our application. The JAR file itself lives in the build/libs
directory and its part of the Gradle standard. The Java files contained in the JAR are inspectable.
As well the tests are created by the Gradle Java tasks in the build/reports/tests/test
directory.
Gradle is very fast on some task's calls because is an Incremental Build System that means it checks that the task has been already executed before actually execute it. So it creates a cache by checking the inputs and the ouputs of a task.
In the compile task: if the source code hasn't changed and the output hasn't been deleted, then Gradle considers the task up-to-date and do not execute it. This allow to save a lot of build time, especially in large projects.
The clean
task deletes the out directory, so the build will be much longer beceause all the tasks will be re-executed.
Gradle takes care of documentation trough the task javadoc
. So we can run that task that generates the folder docs in the output on which documentation in the HTML format is produced.
Compared to a POM file the build.gradle file is much more lighter.
The objective is to build up a standalone executable such that by executing
java -jar gradlejava1-complete-all-0.0.1-SNAPSHOT.jar
we can run our application by using just one line, because all the dependencies are included.
The JAR will contain the application AND the dependencies. The MANIFEST file is different as well: it can contains the main class that has to be executed.
Gradle provides an out-of-the-box solution for this. See the Jar task documentation.
By changing the property baseName
we will change the name of the Jar file. To manipulate the Manifest: manifest(configureClosure)
.
So we need to edit the jar
task itself in the build.gradle:
jar {
baseName = "$project.name-all"
println ">>>> basename: $baseName"
}
and editing the Manifest using the map attribute attributes
:
project.version = '0.0.1-SNAPSHOT'
project.group = 'com.package'
jar {
baseName = "$project.name-all"
manifest {
attributes 'Implementation-Title': 'Gradle all inclusive jar',
'Implementation-Version': project.version,
'Created-By': 'michele.salvucci',
'Main-Class': 'com.package.random.App'
}
println ">>>> basename: $baseName"
println ">>>> manifest.attributes: $manifest.attributes"
}
One method of the jar task is called from
that takes a source file. So we can copy file in the Jar file.
The project contains a configuration container and that container contains all the information we need, in particular the dependencies we need (ConfigurationContainer
). The example in the documentation page of the ConfigurationContainer shows how to copy all dependencies attached to compile into a particular folder.
apply plugin: 'java'
task copyAllDependencies(type: Copy) {
from configurations.compile
into 'allLibs'
}
We can use configuration.compile to return us all the dependency files.
project.configurations.compile
By running the dependencies
task within the help
task it outputs to the console all the dependencies the project has. For some dependencies we need to use runtimeClasspath
instead of implementation
or compile
. So
project.configurations.runtimeClasspath
that is iterable through the method collect
:
project.configurations.runtimeClasspath.collect {File file -> project.zipTree(file)}
We unzip the dependency (sing zipTree()
) file and include that into our own Jar file.
jar {
baseName = "$project.name-all"
manifest {
attributes 'Implementation-Title': 'Gradle all inclusive jar',
'Implementation-Version': project.version,
'Created-By': 'michele.salvucci',
'Main-Class': 'com.package.random.App'
}
from {
project.configurations.runtimeClasspath.collect {File file -> project.zipTree(file)}
}
}
By running the jar
task we can test it and see if works.
java -jar gradlejava1-initial-all-0.0.1-SNAPSHOT.jar
Gradle dependencies are grouped into sets called configurations. Different configurations are used for building classpath for the major two tasks — compile classpath is used for compilation and runtime classpath is used for running the application.
We use implementation configuration in Gradle to hide the internals of our project. Typically, if some classes from a library are only used within the method body then we can use implementation configuration. They are required to compile the project but they are not exposed for the compilation to the depending projects.
Runtime: the dependencies that are only used in runtime when serving the application to either run it or to enhance it.
We need to create a War file (Web Application Archive) that is a colleciton of Java files, Java server pages, Java Servlets and array of XML files, static files and outher resources that may contribute to the application.
For this there will be an additional task called war
.
In an example project, we may have three packages in the src/main/java
:
com.michelesalvucci.config
)com.michelesalvucci.controller
)com.michelesalvucci.services
)
We also have the src/test/java
folder and the src/main/resources
.At the beginning the build.gradle will look like:
plugins {
id 'java'
}
group = 'com.michelesalvucci'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework:spring-webmvc:4.0.3.RELEASE'
implementation 'log4j:log4j:1.2.17'
... other implementation dependencies ...
compileOnly "javax.servlet:javax.servlet-api:3.0.1"
testImplementation "junit:junit:4.11"
}
compileOnly
means that we don't need to provide that dependency with our War file because it can be provided by the Servlet Container itself, so we just need it when we compile the application.
So if we run the build
task that will generates a Jar file. But we can references all the properties in the documentation about the War
class.
So we add it as plugin:
plugins {
id 'java'
id 'war'
}
to have the war
task available.
By execute it the War file will be produced.
plugins {
id 'java'
id 'war'
}
group = 'com.michelesalvucci'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework:spring-webmvc:4.0.3.RELEASE'
implementation 'log4j:log4j:1.2.17'
... other implementation dependencies ...
compileOnly "javax.servlet:javax.servlet-api:3.0.1"
testImplementation "junit:junit:4.11"
}
war {
archiveName = 'mywebapp-${project.version}.war'
}
We can use the copy
task. It accept a type of the class Copy
. It copies files into a destination directory. The Copy type is the behavior of the task.
task deployToTomcat(type: Copy) {
from war.archivePath // war task attribute
to '/path/apache-tomcat-9.0.1/webapps'
}
we can handle the deployment path as an attribute variable:
project.ext.tomcatWebapps = '/path/apache-tomcat-9.0.1/webapps'
...
task deployToTomcat(type: Copy) {
from war.archivePath
to "$tomcatWebapps"
}
After executing the war
and then the deployToTomact
tasks, we can start the tomcat that will pick up the War file from the /webapps
folder. The application will be available from http://localhost:8080/warName.
We can create a dependency task using the dependsOn to ensure execution after the war
task is been executed.
task deployToTomcat(type: Copy, dependsOn: 'war') {
from war.archivePath
to "$tomcatWebapps"
}
In this way, every time we run the deployToTomcat
task Gradle will perform all the task that are mandatory for the war task (compileJava
, processResources
, classes
) and then will execute the war
task ending up with the generation of the War file before the deploy is executed.
To configure a multiproject application is necessary to create a settings.gradle file and a build.gradle file in the root folder of the underlying projects.
In the build.gradle we can user the closure:
subprojects {
}
to write common configurations across the subprojects (such as common dependencies, group, version, repositories, sourceCompatibiliy, targetCompatibility, 'java' or 'spring' plugin to apply). Then we can write single subprojects configurations inside the respective clojures:
project(':projectName') {
dependencies {
...
}
...
}
How we can handle transitive dependencies that are coming from different subprojects?
From the version 3.5 of Gradle the compile dependency configuration was replaced by implementation and api. The idea was to prevent leaking of transitive dependencies in multiproject configurations.
So by default (implementation) a subproject that include another subproject as a dependency doesn't hinerit its transitive dependencies. In this case the referenced dependency should include dependencies by using the api operator that is available by using the java-library plugin.
project(':referenceProjectName') {
apply plugin 'java-library'
dependencies {
...
api 'transitive.dependency'
}
}
project(':referencingProjectName') {
...
dependencies {
...
implementation 'referenceProjectName'
}
}
Of course the api
function has to be used if we are building a library, not an application, because the using of a dependency inside an application must be included through implementation
function.
In the settings.gradle file we have to include all subprojects and indicate the root project name:
include 'subproject-1'
include 'subproject-2'
..
rootProject.name = theName
rootProject.children.each { subproject ->
subproject.buildFileName = "${subproject.name}.gradle"
}
Those constants are defined in the main build.gradle:
theGroup=com.michelesalvucci
theName=project
theVersion=0.0.2-SNAPSHOT
theSourceCompatibility=1.8
Is a good practice, in multiproject builds, to rename the build.gradle files to not get confused:
rootProject.children.each { subproject ->
subproject.buildFileName = "${subproject.name}.gradle"
}
When we perform a Gradle task, such as clean or build, Gradle executes it for each subproject of the multiproject. Gradle is incremental build system, so check that a task has already been executed before execute it, so it perform very fast on rexecution. It's smart enough to understend when a subproject dependency has already been compiled, so it defines a sort of order of execution of build each subproject by itself.
The dependency hirerarchy can be determined by defining dependencies closures in the build.gradle file of the root project as seen in the first pharagraph.
In the end of the build.gradle we can append the deploy task.
The advantage to have a Gradle multiproject build is that we can run task at the root level. For example the documentation is generated for all the subprojects from the root project.
Gradle expose a plugin called FindBugs that performs quality checks on the project's Java source files and generates reports from these checks.
apply plugin: 'findbugs'
The version may need to be overridden in the exposed properties. See the current version at https://findbugs.sourceforge.net.
In the multiproject it's enough to apply the plugin at the root project level (build.gradle).
project(':referenceProjectName') {
apply plugin 'java-library'
apply plugin: 'findbugs'
dependencies {
...
api 'transitive.dependency'
}
}
project(':referencingProjectName') {
...
dependencies {
...
implementation 'referenceProjectName'
}
}
The Gradle command verification/check
invokes findBugs.
It takes some times to generate detailed reports.
The default behavior is to produce an HTML output, but can be switched to HTML:
tasks.withType(FindBugs) {
reports {
xml.enabled false
html.enabled true
}
}
Whenever it finds a rule violation it raises an exception and no longer generates any more bug reports.
The rules of severity and failure level can be customized through the findBugs
closure:
findbugs {
toolVersion = "3.0.1"
ignoreFailures = true
reportLevel = "high"
reportDir = file("$project.buildDir/findbugsReports")
}
Of corse reportLevel
"high" will track down only the most valuable code warnings and errors.
It's a source code analyzer that generates reports as well (like Sonar Lint).
project(':referenceProjectName') {
apply plugin 'java-library'
apply plugin: 'findbugs'
apply plugin: 'pmd'
It is executed by the same Gradle check
task of findBugs.
It is configurable by rules sets:
pmg {
ignoreFailures = true
pmdTest.enabled = false
ruleSets = [
'java-basic',
'java-braces',
'java-clone',
'java-codezie',
...
]
}
tasks.withType(Pmd) {
reports {
xml.enabled=true
html.enabled=true
}
}
The task deploy
depends on the deployment task of the subproject that has to be build and release as final executable.
task deploy('app-web:deployToTomcat') {
doLast {
println ">>>Deploying artifacts"
}
}
the original deploy task (in the build.gradle of the subproject) is of type Copy:
task deployToTomcat(type: Copy, dependsOn: 'build') {
from war
into "$tomcatWebapps"
}
To make changes to the application and redeploy it's enough to make the change and run the delpoy
task.
The Wrapper allows developer to run a Gradle tasks without requiring that Gradle be installed on their system. It takes care of the Gradle version that has to be used.
It can provide a standard for the project, making performing build more reliable.
It is possible to configure a brand new project with Gradle Wrapper by taking advantage of proper plugins supplied by Eclipse or IntelliJ.
We can choose the Gradle version that has to be used and a Gradle project will be automatically created, providing build.gradle
, settings.gradle
and the command file gradlew (gradlew.bat for Windows).
By executing the task build setup/wrapper
a folder named gradle
is also generated containing:
Inside the gradle-wrapper.properties
there are the Gradle Wrapper settings, such as the version (distributionUrl
).
These two files contain the script that executes Gradle for us.
The jar file (gradle-wrapper.jar
) is an executable where the Wrapper code resides.
The first thing is to change the versione in the Gradle wrapper
task (build.gradle):
wrapper {
gradleVersion = '4.0'
}
Then the same Gradle version must be defined in the project settings of the IDE.
Then the property file (distributionUrl
property) has to be checked, it should be updated to the provided version.
Running gradlew version command:
gradlew -v
prints out what version of Gradle is being used, along with Groovy, Ant, JVM and OS.
To perform the clean task and following the build:
gradlew clean build
So if the Gradle version is not available, the Wrapper will download it into the local system (like npm node_modules dipendencies).