Johdatus Gradlen konfigurointiin

Tehdään nyt gradle-projekti alusta asti itse. Tee palautusrepositorioosi uusi hakemisto ja mene hakemistoon.

Kokeile toimiiko koneessasi komento gradle. Huomaa, että esimerkiksi fuksiläppäreissä on asennettuna erittäin vanha gradlen versio. Komennon suorittaminen näyttää mikä versio on kyseessä

mluukkai@melkki:~$ gradle
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :help

Welcome to Gradle 4.4.1.

Jos komento ei toimi tai versio on vanhempi kuin 6.7, kopioi hakemistoon jostain aiemmasta gradle-projektistasi (edellisen viikon tehtävistä) tiedosto gradlew jos käytät Linux tai OSX tai gradlew.bat jos käytät Windowsia ja käytä jatkossa komentoa ./gradlew tai gradlew. Mikäli edelleen ei toimi voit ottaa lähtökohdaksesi täällä olevan tyhjän gradle-projektin.

Aloita antamalla komento gradle:

> Task :help

Welcome to Gradle 6.7.

To run a build, run gradle <task> ...

To see a list of available tasks, run gradle tasks

To see a list of command-line options, run gradle --help

To see more detail about a task, run gradle help --task <task>

For troubleshooting, visit https://help.gradle.org
...

Ohje neuvoo meitä seuraavasti: “To see a list of available tasks, run gradle tasks”, eli kokeillaan komentoa gradle tasks:

> Task :tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

Help tasks
----------
buildEnvironment - Displays all buildscript dependencies declared in root project 'intro_gradle'.
components - Displays the components produced by root project 'intro_gradle'. [incubating]
dependencies - Displays all dependencies declared in root project 'intro_gradle'.
dependencyInsight - Displays the insight into a specific dependency in root project 'intro_gradle'.
dependentComponents - Displays the dependent components of components in root project 'intro_gradle'. [incubating]
help - Displays a help message.
model - Displays the configuration model of root project 'intro_gradle'. [incubating]
outgoingVariants - Displays the outgoing variants of root project 'intro_gradle'.
projects - Displays the sub-projects of root project 'intro_gradle'.
properties - Displays the properties of root project 'intro_gradle'.
tasks - Displays the tasks runnable from root project 'intro_gradle'.

Komento listaa käytettävissä olevat taskit. Gradlen dokumentaatio kuvaa taskeja seuraavasti:

Each project is made up of one or more tasks. A task represents some atomic piece of work which a build performs. This might be compiling some classes, creating a JAR, generating Javadoc, or publishing some archives to a repository.

Taskit ovat siis “komentoja”, joita voimme suorittaa gradle-projekteille.

Gradle-projekti määritellään projektihakemiston juureen sijoitettavan tiedoston build.gradle avulla.

Saat luotua tiedoston suorittamalla taskin init (eli antamalla komennon gradle init).

Valitse basic (type of project), Groovy (build script DSL) ja anna projektille nimi.

Huomaat että operaation jälkeen hakemistoon on tullut tiedoston build.gradle lisäksi muutakin:

$ ls -la
-rw-r--r--  1 mluukkai  984178727   198 Oct 27 18:00 build.gradle
drwxr-xr-x  3 mluukkai  984178727    96 Oct 27 18:00 gradle
-rwxr-xr-x  1 mluukkai  984178727  5766 Oct 27 18:00 gradlew
-rw-r--r--  1 mluukkai  984178727  2763 Oct 27 18:00 gradlew.bat
-rw-r--r--  1 mluukkai  984178727   359 Oct 27 18:00 settings.gradle

Näistä hakemisto .gradle kannattaa gitignoroida. Gradle-projekteissa tulee gitignoroida aina myös hakemisto build mihin kaikki gradle-taskien generoimat tiedostot sijoitetaan. Gradle luokin valmiiksi tilanteeseen sopivan gitignore-tiedoston.

Tavoitteenamme on lisätä projektiin Java-koodia ja JUnit-testejä. Oletusarvoisesti gradle ei ymmärrä Javasta mitään, mutta ottamalla käyttöön java-pluginin, se lisää projektille uusia, Javan kääntämiseen liittyviä taskeja.

Otetaan nyt käyttöön java-plugin lisäämällä tiedostoon build.gradle rivi:

plugins {
    id 'java'
}

Kun nyt suoritetaan komento gradle tasks huomataan että listalla on uusia, java-pluginin lisäämiä taskeja:

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

Build Setup tasks
-----------------
init - Initializes a new Gradle build.
wrapper - Generates Gradle wrapper files.

Documentation tasks
-------------------
javadoc - Generates Javadoc API documentation for the main source code.

...

Verification tasks
------------------
check - Runs all checks.
test - Runs the unit tests.

Voimme nyt siis suorittaa projektille esim. viime viikoilta tutut komennot gradle build ja gradle test.

Jos suoritamme esimerkiksi taskin build eli komennon gradle build, on tulostus seuraava

BUILD SUCCESSFUL in 885ms

Tämä ei vielä oikein kerro mitään. Java-pluginin dokumentaatio selventää asiaa:

Build siis riippuu kahdesta pluginista check ja assemble:

Myös nämä riippuvat muutamasta taskista. Lopulta käy niin, että gradle build suorittaa kaikki seuraavat taskit

compileJava
processResources 
classes
jar
assemble
compileTestJava
processTestResources
testClasses
test
check

Eli build suorittaa koodin käännöksen, paketoinnin jar-tiedostoksi sekä projektiin liittyvät testit.

Ennen kun siirryt eteenpäin, suorita gradle clean, joka poistaa kaikki edellisen komennon luomat tiedostot.

Järkevä editori

Älä käytä tällä kertaa NetBeansia tai muutakaan IDE:ä vaan tee kaikki koodi ja konfiguraatiot tekstieditorilla.

Hyvä vaihtoehto editoriksi on laitoksen koneilta ja fuksikannettavista löytyvä Visual Studio Code

Koodin lisääminen projektiin

Gradle olettaa, että ohjelman koodi sijaitsee projektin juuren alla olevassa hakemistossa src/main/java. Luo hakemisto(t) ja tiedosto src/main/java/Main.java ja sille esim. seuraava sisältö:

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello gradle!");
    }
}

Suorita sitten task compileJava ja tarkastele projektihakemiston sisältöä komennolla tree:

$ tree
.
├── build
│   ├── classes
│   │   └── java
│   │       └── main
│   │           └── Main.class
...

Task compileJava on siis luonut hakemiston build ja sen sisälle käännöksen tuloksena olevan class-tiedoston.

Suorita käännetty koodi menemällä hakemistoon ja antamalla komento java Main:

$ cd build/classes/java/main/
$ java Main
Hello gradle!

Yleensä Java-koodia ei suoriteta käyttämällä suoraan class-tiedostoja. Parempi tapa on pakata koodi jar-tiedostoksi viikon 1 tehtävän 7 tapaan.

Jar-tiedosto muodostetaan gradlen taskilla jar. Help kertoo seuraavaa:

$ gradle help --task jar
Detailed task information for jar

Path
     :jar

Type
     Jar (org.gradle.api.tasks.bundling.Jar)

Description
     Assembles a jar archive containing the main classes.

HUOM komento gradle tulee suorittaa aina projektihakemiston juuressa, eli hakemistossa missä tiedosto build.gradle sijaitsee.

Määritellään taskia varten pääohjelman sijainti lisäämällä seuraava tiedoston build.gradle loppuun:

jar {
    manifest {
        attributes 'Main-Class': 'Main'
    }
}

Huomaa, että tiedoston build.gradle pitää alkaa plugins-määrittelyllä. Jos se ei ole alussa, törmäät seuraavaan virheilmoitukseen:

FAILURE: Build failed with an exception.

* Where:
Build file '/Users/mluukkai/dev/intro_gradle/build.gradle' line: 7

...

@ line 7, column 1.
  plugins {

Palaa nyt projektihakemistoon ja suorita jar-tiedoston generoiva task jar (eli anna komento gradle jar).

Voit suorittaa syntyneen jar-tiedoston seuraavasti (huomaa että tiedoston nimi riippuu projektisi nimestä ja todennäköisesti ei ole gradle-test.jar):

$ java -jar build/libs/gradle-test.jar
Hello gradle!

application-plugin

Aiemmissa tehtävissä olemme pystyneet suorittamaan koodin myös komennolla gradle run.

Komento aiheuttaa kuitenkin nyt virheilmoituksen Task ‘run’ not found in root project.

Syynä tälle on se, että task run ei ole java-pluginin vaan application-pluginin määrittelemä. Otetaan tämä käyttöön muuttamalla tiedoston build.gradle alku muotoon

plugins {
    id 'java'
    id 'application'
}

Itseasiassa java-pluginin määrittelevää riviä ei nyt edes tarvita, sillä application sisältää myös sen määrittelevät taskit.

Komento gradle run aiheuttaa nyt virheen No main class specified.

Pluginin dokumentaatio kertoo, että pääohjelman sisältävä luokka, eli main class tulee määritellä lisäämällä tiedostoon build.gradle seuraava rivi:

application {
    mainClass = 'Main'
}

Nyt ohjelman suorittaminen taskin avulla toimii:

$ gradle run
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE

> Task :run
Hello gradle!

BUILD SUCCESSFUL in 1s

Suorittaminen kannattanee tehdä optiota -q käyttäen, jolloin gradle jättää omat tulosteensa tekemättä:

$ gradle run -q
Hello gradle!

Laajennetaan ohjelmaa siten, että se kysyy Scanner-olion avulla käyttäjän nimeä:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("kuka olet: ");
        String nimi = scanner.nextLine();

        System.out.println("Hello " + nimi);
    }
}

Jos ohjelmasta tehdään jar-tiedosto (suorittamalla komento gradle jar), toimii se odotetulla tavalla:

$ java -jar build/libs/gradle-test.jar
kuka olet:
mluukkai
Hello mluukkai

Jos ohjelma suoritetaan gradlen run-taskin avulla, seurauksena on virhe:

$ gradle run
kuka olet:
Exception in thread "main" java.util.NoSuchElementException: No line found
        at java.util.Scanner.nextLine(Scanner.java:1540)
        at Main.main(Main.java:7)

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':run'.
> Process 'command '/Library/Java/JavaVirtualMachines/jdk1.11.0_101.jdk/Contents/Home/bin/java'' finished with non-zero exit value 1

Syynä tälle on se, että oletusarvoisesti gradlen task run ei liitä terminaalia syötevirtaan System.in. Asia saadaan korjautumaan lisäämällä tiedostoon build.gradle seuraava:

run {
    standardInput = System.in
}

Nyt komento gradle run toimii.

Toinen luokka

Lisätään ohjelmalle luokka, jonka avulla on mahdollista laskea kertolaskuja. Sijoitetaan luokka pakkaukseen ohtu eli tiedostoon src/main/java/ohtu/Multiplier.java

package ohtu;

public class Multiplier {
    private int value;
    public Multiplier(int value) {
        this.value = value;
    }

    public int multipliedBy(int other) {
        return value * other;
    }

}

Käytetään luokkaa pääohjelmasta käsin. Huomaa, että koska luokka on eri pakkauksessa kuin pääohjelma, tulee pakkaus importata:

import java.util.*;
import ohtu.Multiplier;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        Multiplier kolme = new Multiplier(3);
        System.out.println("anna luku ");
        int luku = scanner.nextInt();

        System.out.println("luku kertaa kolme on " + kolme.multipliedBy(luku) );
    }
}

Testit

Tehdään nyt luokalle JUnit-testi. Gradle olettaa, että JUnit-testit sijoitetaan hakemiston src/test/java alle. Sijoitamme testin samaan pakkaukseen kuin testattavan luokan, eli tiedostoon src/test/java/ohtu/MultiplierTest.java

package ohtu;

import static org.junit.Assert.*;
import org.junit.Test;

public class MultiplierTest {

    @Test
    public void kertominenToimii() {
        Multiplier viisi = new Multiplier(5);

        assertEquals(5, viisi.multipliedBy(1));
        assertEquals(35, viisi.multipliedBy(7));
    }

}

Yritetään suorittaa testit komennolla gradle test. Seurauksena on suuri määrä virheilmoituksia. Virheet tapahtuvat taskin compileTestJava eli testien kääntämisen aikana:

$ gradle test

> Task :compileTestJava FAILED
/Users/mluukkai/dev/intro_gradle/src/test/java/MultiplierTest.java:3: error: package org.junit does not exist
import static org.junit.Assert.*;
                       ^
/Users/mluukkai/dev/intro_gradle/src/test/java/MultiplierTest.java:4: error: package org.junit does not exist
import org.junit.Test;
                ^
/Users/mluukkai/dev/intro_gradle/src/test/java/MultiplierTest.java:8: error: cannot find symbol
    @Test
     ^
  symbol:   class Test
  location: class MultiplierTest
/Users/mluukkai/dev/intro_gradle/src/test/java/MultiplierTest.java:12: error: cannot find symbol
        assertEquals(5, viisi.multipliedBy(1));
        ^
  symbol:   method assertEquals(int,int)
  location: class MultiplierTest
/Users/mluukkai/dev/intro_gradle/src/test/java/MultiplierTest.java:13: error: cannot find symbol
        assertEquals(35, viisi.multipliedBy(7));
        ^
  symbol:   method assertEquals(int,int)
  location: class MultiplierTest
5 errors

Syynä virheille on se, että projektimme ei tunne testien importtaamaa koodia:

import static org.junit.Assert.*;
import org.junit.Test;

JUnit-kirjasto on siis ohjelmamme testien käännöksen aikainen riippuvuus.

Riippuvuudet

Käytännössä riippuvuudet ovat jar-tiedostoja, jotka sisältävät käytettävien apukirjastojen, eli tässä tapauksessa JUnitin koodin. Gradlen samoin kuin Mavenin hyvä puoli on se, että ohjelmoijan ei tarvitse itse latailla riippuvuuksia, riittää kun projektin riippuvuudet määritellään tiedostossa build.gradle ja gradle hoitaa automaattisesti riippuvuuksien lataamisen, jos niitä ei koneelta löydy.

Tarvittava määrittely on seuraava:

repositories {
    jcenter()
}

dependencies {
    testImplementation group: 'junit', name: 'junit', version: '4.13'
}

Ensimmäinen osa repositories kertoo gradlelle mistä sen tulee etsiä riippuvuuksia. jcenter on eräs niistä paikoista, johon on talletettu suuri määrä gradlen ja mavenin käyttämiä kirjastoja. Toinen vaihtoehtoinen paikka riippuvuuksien etsimiseen on mavenCentral. repositories-osassa voidaan määritellä myös useita paikkoja joista gradle käy etsimässä projektiin määriteltyjä riippuvuuksia.

Toinen osa määrittelee, että testImplementation-vaiheeseen otetaan käyttöön JUnit-kirjaston versio 4.13. Käytännössä tämä tarkoittaa, että kääntäessään testien koodia gradle liittää JUnitin classpathiin.

Lisättävän riippuvuuden erittelevälle riville voidaan käyttää myös vaihtoehtoista syntaksia, missä riippuvuus, ja sen versio ilmaistaan yhtenä merkkijonona:

dependencies {
    testImplementation 'junit:junit:4.13'
}

Kun suoritamme uudelleen komennon gradle test kaikki toimii.

Rikotaan vielä testi ja varmistetaan että testit huomaavat virheen.

JUnitin uusi versio JUnit5 on ilmestynyt vuosien odotuksen jälkeen jo jonkin aikaa sitten. JUnit5:ssä on monia mielenkiintoisia uudistuksia, mutta valitettavasti työkalutuki on edelleen vielä niin keskeneräinen, että joudumme kurssilla käyttämään vielä vanhaa JUnitia.