Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • akselsa/course-material
  • it1901/course-material
2 results
Select Git revision
Show changes
Commits on Source (174)
Showing
with 525 additions and 307 deletions
...@@ -245,6 +245,9 @@ $RECYCLE.BIN/ ...@@ -245,6 +245,9 @@ $RECYCLE.BIN/
# Windows shortcuts # Windows shortcuts
*.lnk *.lnk
### Maven ###
target/
### Gradle ### ### Gradle ###
.gradle .gradle
build/ build/
......
image: gradle:jdk12 image: maven:3.9.9-eclipse-temurin-17
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
cache:
paths:
- .gradle/wrapper
- .gradle/caches
build: build:
stage: build stage: build
script: script:
- ./gradlew -Pci=gitlab build - mvn "-Dci=gitlab" process-resources
pages:
script:
- mvn "-Dci=gitlab" process-resources
- cp -r target/docs/* public
# only:
# - master
artifacts: artifacts:
paths: paths:
- lectures/build/docs - public
expire_in: 1 week
FROM gitpod/workspace-full-vnc
USER gitpod
# Install custom tools, runtime, etc. using apt-get
# For example, the command below would install "bastet" - a command line tetris clone:
#
# RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/*
#
# More information: https://www.gitpod.io/docs/config-docker/
tasks:
- init: echo "Replace me with a build script for the project."
command: echo "Replace me with something that should run on every start, or just
remove me entirely."
image:
file: .gitpod.Dockerfile
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "gradle run",
"type": "shell",
"command": "gradle",
"args": ["run"]
},
{
"label": "gradle version",
"type": "shell",
"command": "gradle",
"args": ["--version"]
},
{
"label": "java -version",
"type": "shell",
"command": "java",
"args": ["-version"]
},
{
"label": "sdk use java 12.0.2-open",
"type": "shell",
"command": "sdk",
"args": ["use", "java", "12.0.2-open"]
}
]
}
\ No newline at end of file
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.idi.ntnu.no/#https://gitlab.stud.idi.ntnu.no/it1901/course-material)
# course-material # course-material
Course material for IT1901 Course material for IT1901
\ No newline at end of file
target/
.classpath
.project
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it1901</groupId>
<artifactId>antipatterns</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!-- Test with JUnit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiling code -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<encoding>UTF-8</encoding>
<release>13</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<!--
<configuration>
<argLine>${argLine} dash dash add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED</argLine>
</configuration>
-->
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package antipatterns;
public class DataClassWithEffectivelyFinalFields {
private final String name; // should be final
private final int number; // should be final
public DataClassWithEffectivelyFinalFields(final String name, final int number) {
this.name = name;
this.number = number;
}
public String getName() {
return name;
}
public int getNumber() {
return number;
}
}
package antipatterns;
// ok, but see the test
public class DataClassWithLittleLogic {
private String name;
private int number;
public DataClassWithLittleLogic(final String name, final int number) {
this.name = name;
this.number = number;
}
@Override
public String toString() {
return String.format("[DataClassWithLittleLogic name=%s number=%s]", name, number);
}
public DataClassWithLittleLogic() {
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(final int number) {
this.number = number;
}
}
package antipatterns;
// see DataClassWithValidationTest
public class DataClassWithValidation {
private String name;
public DataClassWithValidation(final String name) {
// should use setName to ensure validation logic is run
this.name = name;
}
public DataClassWithValidation() {
}
public String getName() {
return name;
}
public void setName(final String name) {
if (name.startsWith(" ")) {
// should rather use trim() or strip() and check the length
throw new IllegalArgumentException("Name cannot begin with blank");
}
this.name = name;
}
}
package antipatterns;
import java.util.Collection;
import java.util.Iterator;
public class NonEncapsulatedList implements Iterable<String> {
private final Collection<String> names;
public NonEncapsulatedList(final Collection<String> names) {
this.names = names;
}
public Collection<String> getNames() {
return names;
}
public Iterator<String> iterator() {
return names.iterator();
}
}
package antipatterns;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class DataClassWithLittleLogicTest {
private DataClassWithLittleLogic instance1, instance2;
@BeforeEach
public void setup() {
instance1 = new DataClassWithLittleLogic();
instance2 = new DataClassWithLittleLogic("High", 5);
}
@Test
public void testConstructor() {
Assertions.assertEquals("High", instance2.getName());
}
@Test
public void testToString() {
Assertions.assertEquals("[DataClassWithLittleLogic name=High number=5]", instance2.toString());
}
@Test
public void testSetName() {
instance1.setName("Low");
Assertions.assertEquals("Low", instance1.getName());
}
}
package antipatterns;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class DataClassWithValidationTest {
@Test
public void testSetName() {
try {
new DataClassWithValidation(" name starting with blank");
Assertions.fail("Should throw IllegalArgumentException");
} catch (final IllegalArgumentException iae) {
} catch (final Exception iae) {
Assertions.fail("Should throw IllegalArgumentException");
}
try {
new DataClassWithValidation("ok name");
} catch (final Exception iae) {
Assertions.fail();
}
}
@Test
public void testBetterSetName_invalidName() {
Assertions.assertThrows(IllegalArgumentException.class, () -> new DataClassWithValidation(" name starting with blank"));
}
@Test
public void testBetterSetName_validName() {
new DataClassWithValidation("ok name");
}
}
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
...@@ -2,4 +2,4 @@ ...@@ -2,4 +2,4 @@
.gradle .gradle
# Ignore Gradle build output directory # Ignore Gradle build output directory
build build/
# Materiale laget med asciidoctor
Mappa **asciidoctor** inneholder tekstlig materiale, mens **revealjs**-mappa
inneholder lysarkene.
## Hvordan bygge
Bygging av både dokumentasjon og lysark skjer med gradle.
Bruk `gradle asciidoctor` for å generere HTML for det tekstlige og `gradle asciidoctorRevealJs` for lysarkene.
Konvertering kan også gjøres med den generelle `gradle build`.
== Multi-eksempel
Dette eksemplet viser hvordan rigger et multi-prosjekt med gradle,
med del-prosjekter for
- domenelogikk (core)
- brukergrensesnitt med JavaFX og FXML (fx)
- React-basert web-klient (react)
- web-server med JAX-RS og Jersey (jersey)
=== Bygging med Gradle
https://docs.gradle.org/current/userguide/userguide.html[Gradle] er et såkalt byggesystem, som automatiserer oppgaver knyttet til utvikling og kvalitetssikring av programvare, f.eks.
kompilering, kjørings av enhetstester og integrasjonstester, konvertering av dokumentasjon til HTML, pakking som kjørbart program, osv.
Det finnes teknisk sett ikke grenser for hva et byggesystem kan gjøre, generelt handler det om å automatisere bort alt praktisk fikkel,
som ellers hindrer en å jobbe effektivt.
Det finnes flere byggesystem, de viktigste i Java-verdenen er https://maven.apache.org[Maven] og Gradle, mens Javascript-verdenen har sine, bl.a. https://docs.npmjs.com[npm].
I dette prosjektet har vi valgt å bruke Gradle, fordi det er enklere å skrive og lese oppsettet og trigge kjøring, mer fleksiblet og
det vi ønsker å automatisere i akkurat dette prosjektet er innafor det Gradle håndterer godt. For andre Java-prosjekter https://phauer.com/2018/moving-back-from-gradle-to-maven/[kan Maven være et bedre alternativ].
Siden ett av prosjektet bruker Javascript og npm, så
bruker vi et eget Gradle-tillegg som bygger bro til npm.
Gradle er bygget rundt tre sentrale elementer:
- oppgaver (tasks) - det som automatiseres, f.eks. kompilering, kjøring av tester, osv.
- prosjekter - enhetene som oppgaver utføres innenfor
- tillegg (plugins) - angir hvilke oppgaver som hører til et visst type prosjekt.
Oppsettet for bygging består bl.a. i å angi hvilke (del)-prosjekter som inngår i hovedprosjektet,
hvilke avhengigheter det er til eksterne moduler og mellom prosjektene, hvilke tillegg som bør aktiveres for disse prosjektene
(basert på hva slags type prosjekt det er) og hvordan hver type oppgave skal konfigureres.
NOTE: Det litt spesielle med Gradle er at filene for å sette opp et Gradle-bygg egentlig er https://docs.gradle.org/current/userguide/groovy_build_script_primer.html[kode skrevet i det Java-lignende språket Groovy].
Koden brukes primært til konfigurasjon, altså bygge opp beskrivelsen av oppgavene, prosjektene og tilleggene og. API-et er utformet slik at koden skal se deklarativ ut.
En kan flette inn vanlig kode også, hvis en skjønner litt om hvordan API-et virker. Dette gir enorm fleksibilitet, men kan også gjøre filene vanskelige å forstå.
De to viktigste filene for å sette opp et Gradle-bygg er *settings.gradle* og *build.gradle*.
= Kontinuerlig integrasjon
link:slides/ci.html[Lysark]
== Smidig utfordring
Smidig utvikling baserer seg på å levere nyttig funksjonalitet ofte, slik at en 1) kan lære mer om hva som er riktig funksjonalitet og 2) får mest mulig verdi fra det som er utviklet. Dette krever at en itererer raskt og leverer nye versjoner ofte, uten at en samtidig ofrer kvalitet. En kan jo ikke sette nye funksjoner i produksjon flere ganger i uka eller om dagen, hvis en ikke er sikker på at det (fortsatt) virker som det skal.
Dette betyr at både det å sikre kvaliteten underveis og sette nye versjoner i produksjon må koste så lite som mulig. Kvalitetssikring handler mye om testing, men også om måter å sikre at koden har visse kvaliteter som gjør den grei å videreutvikle.
=== Testpyramiden
Testing (ved kjøring av kode) foregår gjerne på flere nivåer. _Enhetstesting_ tester mindre kodeenheter som klasser og metoder isolert fra faktisk bruk i et stor program. Målet er ofte å sjekke korrekthet ved all mulig bruk. _Integrasjonstester_ tester flere moduler og sub-systemer sammen, mer basert på hvordan de er ment å samhandle (bruke hverandre). En tester mer ut fra faktisk bruk, enn all mulig bruk, slik at mengden tester (_mulighetsrommet_) begrenses. _Systemtester_ sjekker at totalsystemet virker som forventet slik det skal kjøre i virkeligheten. En tar gjerne utgangspunkt i _bruksscenarier_ som er identifiser ved design av systemet, slik at bruken blir realistisk. Samtidig skal en jo også sjekke at systemet virker ved unormal og gal bruk, siden det også er realistisk. Noen tester er såkalte _ende til ende_-tester, hvor en simulerer konkret brukerinteraksjon gjennom et UI og ser at alt går rett for seg, f.eks. at databaser oppdateres som de skal og at responsen i UI-et er riktig.
En snakker gjerne om dette som en https://martinfowler.com/articles/practical-test-pyramid.html[testpyramide], fordi mengden tester fra lavere til høyere nivå gjerne går ned fordi det blir uoverkommelig å teste alle mulighetene. Uansett ønsker en å vite at koden som helhet er godt gjennomtestet, altså har høy _testdekningsgrad_ (test coverage). Uavhengig av antall tester er måten en rigger dem på forskjellig. Uten støtte fra testrammeverk og automatisering, vil _kontinuerlig_ testing være ressurskrevende å gjennomføre i praksis.
=== Kodekvalitet
Ved siden av at koden virker, så ønsker en også at kvaliteter som sikrer at den er grei å jobbe med over tid, f.eks. at den er lett å få oversikt over, lese og forstå og modifisere for ulike formål. Dette er kvaliteter som kan sikres ved gjennomlesning, men det er også tidkrevende, og i det er mye å hente om en kan automatisere (deler av) det også, f.eks. sjekke av visse standarder for formatering og koding er fulgt.
== Byggeverktøy og automatisering
Byggeverktøy hjelper en å automatisere mange av de aktivitetene som er nødvendige for å sjekke og sikre kvalitet, og som en uten automatisering ikke ville orket å gjennomføre. Mye er allerede automatisert av moderne IDE-er, f.eks. så kompileres gjerne koden kontinuerig så syntaktiske feil kan angis med røde krøllstreker og koden kan kjøres ved et knappetrykk i stedet for å skrive inn komplekse kommandlinjer i terminalen. Men ut over dette, så bruker en gjerne egne byggeverktøy, bl.a. fordi en ønsker et oppsett som er uavhengig av den enkelte IDE. IDE-en samspiller i stedet med byggeverktøyet, slik at en får det beste av begge verdener. Konfigurasjon gjøres med byggeverktøyet, men IDE-en har gjerne egne editorer for å forenkle jobben og utføre byggekommandoer.
== Konfigurasjon av byggeverktøy
Konfigurasjon av byggeverktøy er komplekst fordi det er så mange variasjonsmuligheter når en skal kompilere, kjøre enhetstester og bygge hele systemer for alle mulige plattformer (språk og rammeverk). Tilnærmingen er likevel nokså lik, selv om detaljene varierer. F.eks. så bygger alle på informasjon om
* hvordan kode og ressurser er strukturert i mapper
* hvilke eksterne og interne moduler (kodebibliotek) en er avhengig av (og til hvilket formål)
* hvilke byggeoppgaver som en ønsker utført (og i hvilke rekkefølge).
For å minimere mengden konfigurasjon så følger en gjerne visse konvensjoner, såkalt https://en.wikipedia.org/wiki/Convention_over_configuration[_convention over configuration_]. For å gjøre det lettere å komme igang finnes ofte _prosjektmaler_ og egne kommandoer for å instansiere disse, altså generere et startoppsett med visse elementer fylt inn spesifikt for anledningen.
Byggeverktøy er avhengig av høy grad av fleksibilitet og utvidbarhet, siden feltet er i rivende utvikling og en er nødt til støtte integrasjon av nye rammeverk og verktøy. Dette gjøres gjerne vha. såkalte _tillegg_ (plug-ins), og mange av standardfunksjonene som kompilering og pakking av filer er ofte realisert som tillegg. Disse krever ytterligere konfigurasjon ut over den generelle informasjonen nevnt over.
== Maven
Det mest modne og utbredte byggeverktøyet for Java-plattformen er https://maven.apache.org[Apache maven], med Googles https://gradle.org[gradle] som en konkurrent i sterk vekst (se https://www.jetbrains.com/lp/devecosystem-2020/java/). Disse har litt ulik terminologi og logikk, men forskjellene er mindre enn en skulle tro ut fra hvor sterke preferanser mange har. Den tydeligste forskjellen er knyttet til at konfigurasjon av gradle tillater bruke av et fullt programmeringsspråk (groovy og i senere tid kotlin), med de fordeler og ulemper det medfører. Her tar vi uansett utgangspunkt i maven, selv om det samme kan gjøres (og nokså likt) med gradle.
=== Standard mappestruktur for kode og ressurser
Med mindre noe annet er konfigurert, krever maven sine standardinnstillinger fire mapper for java (hvis mappen trengs, da):
image::images/javafx-template-structure.png[width=250, float="right"]
** *src/main/java* inneholder "produktiv" java-kode
** *src/main/resources* inneholder _ressurser_, f.eks. *fxml*- og *png*-filer
** *src/test/java* inneholder testkode
** *src/test/resources* inneholder ressurser spesifikke for testene, f.eks. datafiler
I utklippet til høyre ser vi at mappene *src/main/java*, *src/main/resources* og *src/test/java* er i bruk, og hver av disse har en *app*-mappe som tilsvarer java-pakken *app*.
=== Maven-konfigurasjon med pom.xml
Fila *pom.xml* brukes for å konfigurere bygging med maven og en mappe med en slik fil forteller at mappa inneholder et prosjekt satt opp for å bli bygget med maven. Dersom man står i en slik mappe så kan `mvn`-kommandoen bruke for å utføre ulike byggeoppgaver.
Pom-fila (kortform for *pom.xml*-fila) inneholder ulike xml-blokker med diverse informasjon som styrer byggeoppgavene, f.eks:
** nødvendige biblioteker for kompilering, kjøring og testing
** versjon og konfigurasjon av maven-tillegg som trengs i bygget
** konfigurasjon av kompilering (java-versjon) og kjøring (hovedklasse)
Eksemplene under finner du i https://gitlab.stud.idi.ntnu.no/it1901/javafx-template/-/tree/main/javafx-template[javafx-template]-prosjektet på gitlab. I første omgang tar vi utgangspunkt i én-modul-prosjektet *javafx-template*.
==== Avhengigheter
Såkalte _avhengigheter_, f.eks. kode-biblioteker som vårt prosjekt trenger, er angitt i `<dependencies>`-blokka med `<dependency>`-elementer, f.eks. så sier xml-snutten under at koden vår trenger _modulen_ med `artifactId` lik `javafx-controls`, `groupId` lik `org.openjfx` og version lik `16` for å kompilere og kjøre. `groupId`, artifactId og `version` er såkalt _GAV_-koordinater som unikt/globalt identifiserer moduler i maven-økosystemet. Med GAV-koordinatene kan maven spørre et maven-kodelager (repository) lokalt eller på nettet (f.eks. https://mvnrepository.com/repos/central[Maven central]) om den tilsvarende *jar*-fila med den faktiske koden for modulen. Denne kan maven så automatisk laste ned så den kan brukes ved kompilering og kjøring.
[,xml]
----
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>16</version>
</dependency>
----
En kan angi hva slags sammenheng en trenger en avhengighet i med `<scope>`-elementet, f.eks. trenger vi *jupiter*-api-et til testing:
[,xml]
----
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
----
`<scope>`-elementet kan ha ulike verdier for å angi spesifikk bruk av avhengigheter:
* *compile* (standardverdien) - kompilering (og kjøring) av vanlig kode
* *test* - kompilering (og kjøring) av testkode
* *runtime* - trengs ved kjøring, men ikke kompilering
* *provided* - trengs ved kompilering, men ikke ved kjøring (er implisitt med)
Avhengigheten til *jupiter* krever flere moduler, og siden alle skal ha samme versjon, så kunne vi definert det som en konstant i `<properties>`-blokka og så referert til denne i stedet:
[,xml]
----
<properties>
<jupiterVersion>5.7.2</jupiterVersion>
</properties>
...
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${jupiterVersion}</version>
<scope>test</scope>
</dependency>
----
Her deklareres konstanten *jupiterVersion* med verdien *5.7.2* og i `<version>`-elementet vil referansen `${jupiterVersion}` bli byttet ut med denne verdien. Slike konstanter og referanser kan brukes mange steder for å gjøre pom-koden ryddigere og enklere å endre.
==== Maven-tillegg
Maven-tillegg aktiveres og konfigureres i `<plugins>`-blokka med `<plugin>`-elementer:
[,xml]
----
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>16</release>
</configuration>
</plugin>
----
Her aktiveres og konfigureres kompilatoren slik at den bruke Java 16 til kompilering og kjøring (den er automatisk aktivert for java-prosjekter, men vi må konfigurere den for en spesifikk java-versjon). Dette plukkes opp av IDE-en, slik at editoren støtter full Java 16-syntaks, inkluder https://www.baeldung.com/java-text-blocks[_tekstblokker_] (java 15) og https://docs.oracle.com/en/java/javase/16/language/records.html[_verdiobjekter_ (records)].
Maven-tillegg identifiseres med samme slags GAV-koordinater som moduler generelt, og en kan tenke på slike tillegg som spesielle moduler som implementerer nye typer byggeoppgaver. Tillegg-modulene lastes ned automatisk når kjøring av `mvn`-kommandoen (se under) krever det. Hvilke koordinater og konfigurasjonselementer en må bruke, må en nesten finne fra eksempler og søk på nettet. *javafx-template*-prosjektet konfigurerer tre tillegg, *maven-compiler-plugin* for kompilering, *maven-surefire-plugin* for kjøring av tester og *javafx-maven-plugin* for kjøring av javafx-apper. Sistnevnte konfigereres bl.a. med hvilken klasse som er prosjektets app-klasse i `mainClass`-elementet.
=== `mvn`-kommandoen
Med tilleggene over aktivert, så kan en bruke `mvn`-kommandoen for å utføre byggeoppgavene de implementerer:
- `mvn compile` kompilerer koden (som er endret siden sist)
- `mvn test` kjører alle testene (klassen som identifiseres som testklasser)
- `mvn javafx:run` kjører javafx-appen
Argumentet til `mvn` er enten en spesifikk byggeoppgave eller en såkalt _fase_, som er maven sitt hovedkonsept for å organisere og sekvensiere byggeoppgaver. Hvert type prosjekt/modul, f.eks. java-prosjekt, har et sett faser med tilhørende oppgaver. F.eks. så utføres byggeoppgaven *compile:compile* (*compile*-oppgaven til kompilator-tillegget) som en del av fasen *compile*. Når en skriver `mvn compile` så betyr det altså implisitt å kjøre kompilatoren. Faser har en implisitt innbyrdes rekkefølge, så hvis en kjører én fase, så kjøres automatisk fasene tidligere i rekken. F.eks. så kjøres fasene *compile* og *test-compile* automatisk før *test*-fasen når en kjører `mvn test`. Se http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html[maven sin side om dette] for detaljene.
Merk at en kan oppgi flere (uavhengige) byggeoppgaver og/eller faser etter hverandre, f.eks. så kjører en ofte tester før en kjører programmet med `mvn test javafx:run`.
`<configuration>`-elementene inni `<plugin>`-deklarasjonen av tilleggene angir diverse verdier som tilleggene bruker når deres byggeoppgaver kjøres, om disse kjøres direkte eller som en del av en fase. Noen ganger er det nyttig å kjøre byggeoppgaver med spesifikke alternative verdier, f.eks. kan en ønske å kjøre én bestemt test(klasse) og ikke alle sammen på en gang, eller kjøre en alternativ app-klasse i stedet for den som er angitt i pom-en. Dette lar seg ofte gjøre ved å angi "-Dkonstant=verdi" som kommandolinjeargument, hvor *konstant* og *verdi* er spesifikk for tillegget og anledningen. (Anførselstegnet brukes for å unngå at spesielle tegn i konstantnavnet og verdien tolkes som noe annen en vanlige tegn.) F.eks. kjører man testklassenn *pack.TestClass* med `mvn test "-Dtest=pack.TestClass"` og app-klassen *pack.AppClass* med `mvn javafx:run "-DmainClass=pack.AppClass"`. Som regel er konstantnavnet det samme som det tilsvarende konfigurasjonselementen.
=== Resultater fra byggingen
Hensikten med hver byggeoppgave er ofte å generere filer, ofte kalt _artifakter_, som er _avledet_ fra filer i prosjektet eller byggeoppgaver kjørt tidligere. F.eks. så er er resulatet fra kompilering *class*-filer og kjøring av tester gir testrapporter. De fleste slike byggeresultater (litt avhengig av konfigureringen) havner i *target*-mappa, f.eks. havner *class*-filene i *target/classes*-mappa (sammen med ressurser fra *process-resources*-fasen). Disse klassene (og ressursene) plukkes opp av *jar:jar*-oppgaven i *package*-fasen og pakkes i en *jar*-fil som også havner i *target*. Til slutt kan *deploy*-fasen brukes til å laste *jar*-filen opp på en server, så den kan lastes ned av andre og evt. settes i produksjon.
En nyttig fase som ofte inkluderes først (eller alene) i en `mvn`-kommando er *clean*. Den fjerner hele *target*-mappa, slik at byggingen starter med "blanke ark". Dette kan gjøre det enklere å finne feil i oppsettet, fordi en da inngår å kjøre byggeoppgaver på "gamle" byggeresultater.
== Modularisering
En ønsker ofte å dele opp et prosjekt i flere _moduler_, slik at hver del er relativt isolert fra og uavhengig av hverandre. Dette gjør delene mer gjenbrukbare og kan gjøre det ryddigere og enklere å koordinere gruppen av utviklere som utviklere prosjektet som helhet. F.eks. kan en ha separate moduler for kjernekoden, brukergrensesnittet og web-tjenesten. Med klare skiller mellom modulene, så sauses de ikke så lett sammen og utviklerne går mindre i beina på hverandre.
Det kan fortsatt være avhengigheter mellom modulene, men disse er eksplisitte, på samme måte som moduler har avhengigheter til biblioteksmoduler skrevet av andre og gjort tilgjengelig på sentrale tjenere. Internt i en virksomhet kan modulene i et prosjekt fungere som bibliotelsmoduler i et annet. Modularisering er et viktig virkemiddel for å begrense kompleksiteten til store systemer.
=== Flér-modul-prosjekter i maven
Maven støtter oppdeling av et stort prosjekt i flere moduler. Hver av modulene blir på en måte selvstendige prosjekter under et felles hovedprosjekt. En flérmodul-variant av *javafx-template*-prosjektet finner du i https://gitlab.stud.idi.ntnu.no/it1901/javafx-template/-/tree/main/modules-template[modules-template]-mappa. Denne mappa utgjør hovedmodulen, med moduler for kjernekode og ui i hver sine undermapper, henholdsvis *core* og *ui*. Merk at en kan ha flere nivåer enn dette, altså undermoduler med moduler under der igjen, men her forholder vi oss til bare to nivåer. For et større eksempel, se https://gitlab.stud.idi.ntnu.no/it1901/todo-list[todo-list].
En hovedmodul angir undermodulene i pom-fila si, i `<module>`-elementer i `<modules>`-blokka:
[,xml]
----
<modules>
<module>core</module>
<module>ui</module>
</modules>
----
Undermodulene referere tilbake til hovedmodulen ved å angi GAV-koordinatene i `<parent>`-elementet:
[,xml]
----
<parent>
<groupId>it1901</groupId>
<artifactId>modules-template</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
----
Undermodulene deklarerer egne *artifactId*-verdier, mens *groupId* og *version* arves fra hovedmodulen. Dermed får hoved- og undermoduler egne GAV-koordinater hvor *groupId*- og *version*-elementene er felles. Disse kan brukes for å angi avhengigheter mellom undermodulene, f.eks. fra ui-modulen til kjernemodulen.
Hovedprosjektet blir mer nyttig ved at pom-fila kan inneholde konstanter, avhengigheter og konfigurasjon av tillegg som er felles for undermodulene og gjøre det lettere å sikre at de bruker de samme konstantverdiene, avhengighetene og tilleggene.
Felles avhengigheter deklareres inni `<dependencyManagement>`-blokka og tillegg inni `<pluginManagement>`-blokka. I undermoduler angir man dem som vanlig, men kan utelate detaljer der deklarasjonene i hovedmodulen duger. I ui-modulen aktiveres f.eks. *maven-compiler-plugin*- og *maven-surefire-plugin*-tilleggene med versjonene og konfigurasjonene angitt i hovedmodulen, og i tillegg deklareres *javafx-maven-plugin*-tillegget, som _ikke_ er nevnt i hovedmodulen:
[,xml]
----
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.6</version>
<configuration>
<mainClass>app.App</mainClass>
</configuration>
</plugin>
</plugins>
----
=== mvn-kommandoer i flér-modul-prosjekter
`mvn`-kommandoer med faser som argument, f.eks. *clean*, *compile* og *test*, kan utføres i mappa for hovedprosjektet. Fasene blir da utført i underprosjektene i en rekkefølge utledet fra innbyrdes avhengigheter. Hvis f.eks. *ui*-modulen avhenger av *core*-modulen, så vil kommandoen `mvn compile` utført i hovedmodulen gjøre at *compile*-fasen først utføres i *core*-modulen og deretter i *ui*-modulen, siden det ligger i kortene at sistnevnte er avhengig av byggeresultatene fra førstnevnte.
Det er litt verre med kjøring av spesifikke byggeoppgaver, f.eks. *javafx:run*. Det går bare hvis tilsvarende maven-tillegg er aktivert for hovedmodulen og alle undermodulene. Ofte må man angi *-pl*-opsjonen etterfulgt av undermodulen for å begrense kjøringen til den relevante modulen. F.eks. kan man kjøre `mvn -pl ui javafx:run` i mappa for hovedmodulen i stedet for kjøre `cd ui` etterfulgt av `mvn javafx:run`.
== IDE-støtte for maven
De fleste IDE-er gjenkjenner mapper med *pom.xml*-fil som prosjekter konfigurert med maven og kan rette seg litt etter konfigurasjonen, f.eks. for å sikre at (implisitt) kompilering og smarte editorfunksjoner bruker riktig Java-versjon og gi tilgang til pakker og klasser i biblioteker som er deklarert som avhengigheter.
Noen IDE-er går enda lenger og tilbyr editorer for å redigere pom-filer og paneler og dialoger for å utføre utføre byggeoppgaver tilsvarende `mvn`-kommandoer.
== Bygging i gitlab
Gitlab har støtte for å kjøre de samme byggeoppgavene som en kan kjøre lokalt. Ut over å være en ekstra kvalitetssikring, så hjelper det å gjøre oppsettet av prosjektet uavhengig av oppsettet på den enkelt utvikler sin maskin. Det er lett å komme i skade for å gjøre kjøring av byggeoppgaver avhenger av lokalt oppsett, så dette sikrer et _reproduserbart_ bygg.
En fil ved navn *.gitlab-ci.yml* brukes for å angi det felles byggeoppsettet, og når ulike byggeoppgaver skal utføres. F.eks. kan en be om at alle testene kjøres hver gang ny kode innlemmes i hovedgreina til prosjektet.