Salta al contenuto
Note per Studenti Strumenti di sviluppo - git, Maven e Docker

Strumenti di sviluppo - git, Maven e Docker

In questa pagina 5

Prima di scrivere servlet e pagine servono tre strumenti che non fanno parte dell'applicazione, ma ne regolano la vita: git per condividere e ricordare il codice, Maven per costruire (build) il pacchetto da distribuire, Docker per eseguirlo sempre nello stesso ambiente. Sono tre dei temi del programma ufficiale e servono nella prova di programmazione, dove si consegna un progetto che un'altra persona deve poter compilare e avviare.

L'esempio è la piccola applicazione di prenotazione delle aule introdotta in Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →: tabelle aula e prenotazione, classi Aula, Prenotazione, AulaDAO, PrenotazioneDAO, AuleServlet, PrenotazioniServlet. Il progetto si chiama aule e ha questa struttura, che le tre sezioni costruiscono un pezzo alla volta:

aule/
├── .gitignore                 (git: che cosa NON salvare)
├── pom.xml                    (Maven: descrizione e dipendenze)
├── Dockerfile                 (Docker: come costruire l'immagine)
├── docker-compose.yml         (Docker: Tomcat + PostgreSQL insieme)
├── sql/schema.sql             (tabelle e dati di esempio)
└── src/main/
    ├── java/it/unipd/aule/    (model, dao, web, mvc, rest, filter)
    └── webapp/                (pagine HTML, JS, WEB-INF/web.xml, META-INF/context.xml)

Git

Git è un sistema di controllo di versione distribuito (distributed version control system): ogni copia del progetto (repository, abbreviato repo) contiene l'intera storia, quindi si lavora senza rete e ci si sincronizza quando serve. Risolve tre problemi: tornare a una versione precedente, capire chi ha cambiato che cosa e perché, lavorare in più persone sugli stessi file.

Le tre aree e il commit

cartella di lavoro  ==git add==>  area di staging  ==git commit==>  repository (.git)
 (i file che modifichi)        (le modifiche scelte           (la storia: commit collegati)
                                per il prossimo commit)
  • La cartella di lavoro (working tree) contiene i file che si modificano.
  • L'area di staging (staging area, detta anche index) raccoglie le modifiche scelte per il prossimo commit: serve a fare commit piccoli e coerenti anche se si sono toccati molti file.
  • Un commit è un'istantanea (snapshot) dell'intero progetto più un messaggio, l'autore e il riferimento al commit (o ai commit) precedente. È identificato da un hash SHA di 40 caratteri esadecimali (di solito se ne scrivono 7); cambiare qualunque cosa nella storia cambierebbe gli hash successivi.
  • Un branch (ramo) è soltanto un puntatore mobile a un commit: crearne uno costa quasi niente. main è il ramo principale per convenzione. HEAD è il puntatore a ciò che è attualmente estratto, di solito il ramo corrente.

Primo commit

bash
$ git status --short
?? schema.sql
$ git add schema.sql
$ git commit -m "Schema iniziale con tre aule"
[main (root-commit) 12f56a6] Schema iniziale con tre aule
 1 file changed, 4 insertions(+)
 create mode 100644 schema.sql

?? significa "file non tracciato" (untracked). git add lo mette in staging, git commit lo registra: root-commit perché è il primo, 12f56a6 sono le prime 7 cifre dell'hash. Prima di un commit sono utili:

Comando Effetto
git status file modificati, in staging, non tracciati; ramo corrente
git diff differenze tra cartella di lavoro e staging (righe tolte con -, aggiunte con +)
git diff --staged differenze tra staging e ultimo commit, cioè ciò che entrerebbe nel commit
git log --oneline --graph --all la storia, un commit per riga, con il disegno dei rami
git add -p sceglie a pezzi quali modifiche mettere in staging

Messaggi di commit. Una riga che dice che cosa cambia ("Aggiunge il controllo delle sovrapposizioni"), non come ("modifiche"); commit piccoli, uno per scopo, così si possono rivedere e annullare uno alla volta.

.gitignore

Si salva il sorgente, non ciò che si può rigenerare. Il file .gitignore elenca i percorsi che git ignora:

target/
*.war
.idea/
*.iml
.vscode/

target/ è la cartella dove Maven mette classi e war: ricostruibile con un comando, quindi fuori dal repository. Fuori anche segreti (password del database, chiavi): una volta in un commit restano nella storia anche se si cancella il file.

Rami e merge

Per sviluppare una funzione senza toccare main si crea un ramo:

bash
$ git switch -c rinomina-lab
Switched to a new branch 'rinomina-lab'

git switch -c nome crea il ramo e ci si sposta (git switch nome si sposta soltanto; il vecchio git checkout fa le due cose più altre). I commit fatti da qui in poi avanzano il ramo rinomina-lab e lasciano main dov'era. Per riportare il lavoro in main ci si sposta su main e si esegue git merge rinomina-lab. Si presentano due casi.

Fast-forward. Se main non ha avuto altri commit dalla creazione del ramo, main è un antenato del ramo: git sposta semplicemente il puntatore in avanti, senza creare commit.

prima:  A---B  (main)            dopo il merge:  A---B---C---D  (main, rinomina-lab)
             \
              C---D  (rinomina-lab)

Commit di merge. Se invece anche main è avanzato, le storie sono divergenti e git crea un commit di merge, con due genitori, che riunisce i due rami. Se le modifiche toccano parti diverse dei file la fusione è automatica (messaggio Merge made by the 'ort' strategy).

bash
$ git branch -d rinomina-lab        # cancella il ramo già fuso (-D lo cancella anche se non fuso)

Conflitti

Un conflitto (merge conflict) nasce quando i due rami hanno modificato le stesse righe di uno stesso file in modo diverso: git non può sapere quale versione tenere. Si ferma e lo dice:

bash
$ git merge rinomina-lab
Auto-merging schema.sql
CONFLICT (content): Merge conflict in schema.sql
Automatic merge failed; fix conflicts and then commit the result.

Nel file compaiono i marcatori:

<<<<<<< HEAD
INSERT INTO aula (nome, posti) VALUES ('Lab Info', 36);
=======
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 30);
>>>>>>> rinomina-lab

Tra <<<<<<< HEAD e ======= c'è la versione del ramo corrente; tra ======= e >>>>>>> rinomina-lab quella del ramo che si sta fondendo. Si risolve così:

  1. aprire il file e decidere il risultato (una delle due, o una combinazione: qui il nome nuovo con i posti nuovi), cancellando i tre marcatori;
  2. git add schema.sql: dice a git "risolto";
  3. git commit (il messaggio di merge è già pronto, --no-edit lo accetta).

Per rinunciare, git merge --abort riporta tutto com'era prima del merge. Un conflitto non è un errore: è la richiesta di una decisione umana. Dopo averlo risolto si ricompila e si prova il programma, perché due modifiche prive di conflitto testuale possono comunque essere incompatibili tra loro (per esempio un metodo rinominato in un ramo e usato nell'altro). La procedura completa, con tutti i comandi e le uscite, è nell'Esercizio - Git con branch, merge e conflitto (applicazione web).

Il remoto

Un remoto (remote) è un altro repository, di solito su un server (GitHub, GitLab, un server dell'università), che il gruppo usa come punto di scambio. Il remoto principale si chiama origin.

Comando Effetto
git clone URL copia un repository remoto sul proprio computer, già collegato a origin
git fetch scarica i commit nuovi del remoto senza toccare i propri rami (aggiorna origin/main)
git pull fetch + merge del ramo remoto in quello corrente
git push invia al remoto i propri commit; la prima volta git push -u origin main memorizza il ramo remoto di riferimento (-u, upstream)

origin/main è la copia locale dell'ultimo stato noto di main sul remoto: si aggiorna solo con fetch o pull. Se qualcuno ha pubblicato prima di te, git push viene rifiutato:

bash
$ git push
To https://example.org/aule.git
 ! [rejected]        main -> main (fetch first)
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.

Non è un guasto: git impedisce di sovrascrivere il lavoro altrui. Si fa git pull (che può produrre un conflitto, da risolvere come sopra) e poi si ripete il push. Mai git push --force su un ramo condiviso: riscrive la storia degli altri.

Annullare

Situazione Comando
scartare le modifiche non ancora in staging di un file git restore file (irreversibile: le modifiche non erano mai state salvate)
togliere un file dallo staging git restore --staged file
annullare un commit già pubblicato git revert hash: crea un nuovo commit che fa il contrario, la storia non si riscrive
mettere da parte il lavoro a metà git stash e poi git stash pop
tornare indietro cancellando commit locali git reset --hard hash (pericoloso: non usarlo su commit già pubblicati)

Per segnare una consegna si usa un tag: git tag -a v1.0 -m "Prima consegna".

Flusso di lavoro in gruppo

Una convenzione semplice, adatta a un progetto di gruppo:

  1. main contiene sempre codice che compila e funziona; non ci si committa direttamente;
  2. per ogni funzione o correzione: git switch -c nome-funzione da un main aggiornato (git pull);
  3. commit piccoli e frequenti sul ramo; ogni tanto git pull origin main per portarsi dietro le novità ed evitare conflitti enormi alla fine;
  4. a lavoro finito il ramo viene pubblicato (git push -u origin nome-funzione) e si apre una pull request (o merge request): non è una funzione di git ma della piattaforma ospitante, e serve a far rivedere il codice agli altri prima del merge;
  5. dopo il merge il ramo si cancella.

Per ridurre i conflitti: file piccoli con una sola responsabilità, persone diverse su file diversi, nessuna riformattazione di interi file mentre altri lavorano.

Maven

Apache Maven è uno strumento di build e gestione delle dipendenze per progetti Java. Si basa su due idee: convenzione sulla configurazione (se il progetto segue la struttura standard, serve pochissima configurazione) e descrizione dichiarativa (nel file pom.xml, Project Object Model, si dice che cosa il progetto è e di che cosa ha bisogno, non i comandi per compilarlo).

Struttura standard

aule/
├── pom.xml
├── src/main/java/         codice Java dell'applicazione
├── src/main/resources/    file copiati nel classpath (properties, ...)
├── src/main/webapp/       radice dell'applicazione web: HTML, JSP, WEB-INF/web.xml
├── src/test/java/         test (JUnit)
└── target/                tutto ciò che Maven produce (classi, war): è in .gitignore

Il POM

xml
<?xml version="1.0" encoding="UTF-8"?>
<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>

  <!-- coordinate: identificano l'artefatto come groupId:artifactId:version -->
  <groupId>it.unipd.aule</groupId>
  <artifactId>aule</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>               <!-- il risultato è target/aule.war -->

  <properties>
    <maven.compiler.release>21</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!-- l'API servlet la fornisce Tomcat: serve per compilare, ma NON va nel war -->
    <dependency>
      <groupId>jakarta.servlet</groupId>
      <artifactId>jakarta.servlet-api</artifactId>
      <version>6.0.0</version>
      <scope>provided</scope>
    </dependency>
    <!-- JSTL: Tomcat non la include, quindi va nel war (scope compile, il predefinito) -->
    <dependency>
      <groupId>jakarta.servlet.jsp.jstl</groupId>
      <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
      <version>3.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.web</groupId>
      <artifactId>jakarta.servlet.jsp.jstl</artifactId>
      <version>3.0.1</version>
    </dependency>
    <!-- JSON per le servlet REST -->
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.11.0</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>aule</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.4.0</version>
      </plugin>
    </plugins>
  </build>
</project>

Che cosa dice, punto per punto:

scope compilazione test dentro il war esempio
compile (predefinito) sì sì sì Gson, JSTL
provided sì sì no (la fornisce il container) API servlet
runtime no sì sì driver JDBC (se lo si mette nel war)
test solo test sì no JUnit

L'API servlet è provided: se finisse anche nel war ci sarebbero due copie delle stesse classi (quella dell'applicazione e quella di Tomcat) e comparirebbero errori di caricamento difficili da capire. Il driver di PostgreSQL non è nel pom perché sta nella cartella lib di Tomcat (sezione Docker), dove serve al pool di connessioni del container.

Il ciclo di vita

Maven definisce un ciclo di vita (lifecycle) predefinito: sequenza di fasi (phases) in ordine fisso.

fase che cosa fa
validate controlla che il progetto sia corretto
compile compila src/main/java in target/classes
test compila ed esegue i test
package crea il pacchetto (qui target/aule.war)
verify controlli sul pacchetto (test di integrazione)
install copia il pacchetto nel repository locale, per usarlo da altri progetti
deploy lo copia in un repository remoto condiviso

Regola fondamentale: lanciare una fase esegue prima tutte le precedenti. mvn package compila, esegue i test e poi impacchetta. Il lavoro vero lo fanno i plugin: ogni fase è collegata (bound) a un obiettivo (goal) di un plugin; per un progetto war compile usa compiler:compile, test usa surefire:test, package usa war:war. Esiste anche un ciclo separato, clean, che cancella target/.

bash
$ mvn clean package        # cancella target/, poi compila, prova e crea target/aule.war
$ mvn -DskipTests package  # senza eseguire i test
$ mvn dependency:tree      # mostra le dipendenze, anche le transitive

Il file target/aule.war è ciò che si distribuisce: basta copiarlo nella cartella webapps di Tomcat.

Docker

Un'applicazione funziona "sul mio computer" per mille ragioni (versione di Java, di Tomcat, librerie, variabili d'ambiente). Docker risolve il problema confezionando l'applicazione con il suo ambiente.

  • Un'immagine (image) è un modello di sola lettura: file system con Tomcat, Java e l'applicazione, più i comandi di avvio. È fatta di strati (layers), uno per istruzione del Dockerfile, che si riusano se non cambiano: ricostruire dopo aver modificato una riga di codice non riscarica Tomcat.
  • Un container è un'istanza in esecuzione di un'immagine: un processo isolato (file system, rete e processi propri) che condivide il kernel del sistema ospite. Non è una macchina virtuale completa, quindi parte in un attimo e pesa poco. Dall'immagine si possono avviare più container; quando il container viene cancellato si perde ciò che ha scritto, salvo i volumi.
  • Un registro (registry, per esempio Docker Hub) è un archivio di immagini: tomcat:10.1-jdk21 significa "immagine tomcat, etichetta (tag) 10.1-jdk21".

Il Dockerfile

Il Dockerfile è la ricetta per costruire un'immagine. Qui ne servono due fasi (multi-stage build): la prima, con Maven, compila il war; la seconda parte da Tomcat e vi copia solo il war, senza portarsi dietro Maven e i sorgenti.

dockerfile
# --- fase 1: compilare il war con Maven ---
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -q dependency:go-offline          # scarica le dipendenze: questo strato resta in cache finché il pom non cambia
COPY src ./src
RUN mvn -q package -DskipTests            # produce /app/target/aule.war

# --- fase 2: immagine finale con solo Tomcat e il war ---
FROM tomcat:10.1-jdk21
RUN rm -rf /usr/local/tomcat/webapps/*    # via le applicazioni di esempio
ADD https://repo1.maven.org/maven2/org/postgresql/postgresql/42.7.3/postgresql-42.7.3.jar /usr/local/tomcat/lib/
COPY --from=build /app/target/aule.war /usr/local/tomcat/webapps/ROOT.war
EXPOSE 8080
Istruzione Significato
FROM immagine immagine di partenza; AS build dà un nome alla fase
WORKDIR cartella di lavoro per le istruzioni successive
COPY origine destinazione copia file dal contesto di build (la cartella del progetto); --from=build li prende dalla fase precedente
RUN comando esegue un comando durante la costruzione e salva il risultato come nuovo strato
ADD URL destinazione come COPY, ma accetta anche un URL
EXPOSE 8080 documenta la porta su cui il processo ascolta (non la pubblica)

Si copia prima pom.xml e si scaricano le dipendenze, poi il codice: finché il pom non cambia, lo strato delle dipendenze è in cache e le ricostruzioni sono veloci. Il war si chiama ROOT.war perché Tomcat pubblica ROOT alla radice (http://localhost:8080/) invece che sotto /aule.

bash
$ docker build -t aule-web .                       # costruisce l'immagine e le dà il nome aule-web
$ docker run --name aule -p 8080:8080 aule-web      # avvia un container; la porta 8080 del computer va alla 8080 del container
$ docker ps                                         # container in esecuzione
$ docker logs -f aule                               # segue il log (qui il catalina.out di Tomcat)
$ docker stop aule && docker rm aule                # ferma e cancella il container

-p computer:container pubblica la porta; senza, il container non è raggiungibile da fuori.

docker compose

Una sola applicazione non basta: serve anche il database. Docker Compose descrive in un file YAML più servizi (container) e come collegarli, e li avvia con un comando.

yaml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_DB: aule
      POSTGRES_USER: aule
      POSTGRES_PASSWORD: aule
    volumes:
      - dati:/var/lib/postgresql/data                              # i dati sopravvivono a "docker compose down"
      - ./sql/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro # eseguito solo alla prima creazione del database
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U aule -d aule"]
      interval: 5s
      timeout: 3s
      retries: 10

  web:
    build: .                      # usa il Dockerfile della cartella corrente
    ports:
      - "8080:8080"               # porta del computer : porta del container
    depends_on:
      db:
        condition: service_healthy

volumes:
  dati:
  • Il servizio db usa l'immagine ufficiale di PostgreSQL, che al primo avvio crea database e utente indicati dalle variabili d'ambiente POSTGRES_*, ed esegue gli script SQL della cartella /docker-entrypoint-initdb.d/: montando lì sql/schema.sql il database nasce già con le tabelle e i dati di esempio.
  • Il volume dati è una cartella gestita da Docker, montata dove PostgreSQL scrive i file: sopravvive alla cancellazione del container. Lo script di inizializzazione però gira solo se il volume è vuoto: per ripartire da zero, docker compose down -v.
  • Su una rete creata da Compose i servizi si raggiungono con il nome del servizio: nel context.xml di Tomcat l'URL JDBC è jdbc:postgresql://db:5432/aule, con db come host.
  • depends_on con service_healthy fa partire Tomcat solo dopo che PostgreSQL risponde (pg_isready); altrimenti il pool di connessioni troverebbe il database non ancora pronto.
  • build: . costruisce l'immagine web con il Dockerfile di prima; ports pubblica la 8080.
bash
$ docker compose up --build -d     # costruisce, avvia in background (-d); l'applicazione è su http://localhost:8080
$ docker compose ps                # stato dei servizi
$ docker compose logs -f web       # log del solo servizio web
$ docker compose down              # ferma e cancella i container (i dati restano nel volume)

Così chiunque abbia Docker avvia l'intera applicazione con docker compose up, senza installare Java, Tomcat né PostgreSQL. La configurazione con la password scritta nel file va bene per lo sviluppo; in produzione le credenziali arrivano da variabili d'ambiente o da segreti, mai dal repository.

Errori tipici

  • Committare target/, file .war o password nel repository.
  • Lavorare direttamente su main in gruppo, o fare commit enormi con messaggi come "modifiche".
  • Credere che un merge senza conflitti sia sicuramente corretto: dopo il merge si compila e si prova.
  • Usare git push --force o git reset --hard su commit già condivisi.
  • Mettere l'API servlet con scope compile: il war contiene una seconda copia delle classi di Tomcat.
  • Confondere immagine e container: l'immagine non cambia mai a runtime, e ciò che il container scrive sparisce con lui (salvo volumi).
  • Dimenticare -p: il container gira ma da fuori non si raggiunge.
  • Usare localhost come host del database nel context.xml: dentro il container di Tomcat localhost è il container stesso, non quello di PostgreSQL.

Domande d'esame

1. Qual è la differenza tra un merge fast-forward e un merge con commit di merge? Quando nasce un conflitto? Traccia. Fast-forward quando il ramo corrente è un antenato di quello da fondere: si sposta solo il puntatore. Se entrambi i rami hanno nuovi commit, git crea un commit con due genitori. Il conflitto nasce quando i due rami hanno modificato le stesse righe: git inserisce i marcatori, si modifica il file, git add, git commit.

2. Che cosa succede lanciando mvn package su un progetto con packaging war? Perché l'API servlet ha scope provided? Traccia. Si eseguono in ordine le fasi validate, compile, test e package (compilazione, test, war:war), producendo target/<finalName>.war. L'API servlet serve per compilare ma la fornisce Tomcat: nel war non deve esserci.

3. Spiegare la differenza tra immagine e container, e a che cosa servono il Dockerfile e docker compose nel progetto delle aule. Traccia. L'immagine è il modello di sola lettura a strati, il container un'istanza in esecuzione con uno strato scrivibile. Il Dockerfile (a due fasi) compila il war con Maven e lo copia in Tomcat; compose avvia Tomcat e PostgreSQL insieme, con rete interna (host db), volume per i dati e script schema.sql di inizializzazione.

Versione ripasso

Git. Controllo di versione distribuito: ogni repository ha tutta la storia. Tre aree: cartella di lavoro, staging (git add), repository (git commit). Il commit è un'istantanea con hash, autore e messaggio; il branch è un puntatore mobile a un commit; HEAD indica dove si è.

bash
$ git add schema.sql
$ git commit -m "Schema iniziale con tre aule"
$ git switch -c rinomina-lab        # crea il ramo e ci si sposta
$ git merge rinomina-lab            # da main: fonde il ramo
  • git status, git diff (lavoro contro staging), git diff --staged, git log --oneline --graph --all.
  • .gitignore: target/, *.war; mai segreti nel repository.
  • Conflitto, come appare nel file e come si risolve (scelta: nome nuovo, posti nuovi):
<<<<<<< HEAD
INSERT INTO aula (nome, posti) VALUES ('Lab Info', 36);
=======
INSERT INTO aula (nome, posti) VALUES ('Laboratorio di Informatica', 30);
>>>>>>> rinomina-lab
  • Fast-forward: main non è avanzato, si sposta solo il puntatore. Commit di merge: storie divergenti, due genitori.
  • Conflitto: stesse righe modificate nei due rami. Marcatori <<<<<<< HEAD (ramo corrente), =======, >>>>>>> ramo. Si modifica il file, git add file, git commit; git merge --abort per rinunciare.
  • Remoto origin: clone, fetch (non tocca i rami), pull (= fetch + merge), push (la prima volta -u origin main). Push rejected (fetch first): prima git pull, poi di nuovo push. Mai --force su rami condivisi.
  • Annullare: git restore file (irreversibile), git revert hash (nuovo commit contrario, per commit pubblicati), git stash, git reset --hard (solo commit locali).
  • Remoto, in sintesi:
Comando Effetto
git clone URL copia il repository, già collegato a origin
git fetch scarica i commit nuovi, non tocca i rami locali
git pull fetch + merge nel ramo corrente
git push -u origin main invia i commit; -u ricorda il ramo remoto
  • Altri comandi: git branch -d ramo (cancella un ramo già fuso), git tag -a v1.0 -m "Prima consegna" (segna una consegna), git add -p (sceglie a pezzi), git restore --staged file (toglie dallo staging).
  • In gruppo: main sempre funzionante, un ramo per funzione, commit piccoli, pull frequenti, pull request per la revisione, ramo cancellato dopo il merge.

Maven. pom.xml dichiarativo; convenzione: src/main/java, src/main/resources, src/main/webapp, src/test/java, risultato in target/.

  • Il POM contiene, in ordine: modelVersion, coordinate e packaging, properties (maven.compiler.release, project.build.sourceEncoding), dependencies, build (finalName aule, plugin maven-war-plugin).
  • Coordinate groupId:artifactId:version (it.unipd.aule:aule:1.0-SNAPSHOT), packaging war, maven.compiler.release 21.
  • Dipendenze scaricate da Maven Central con le transitive, tenute in .m2/repository. Scope: compile (nel war), provided (non nel war, la dà il container: API servlet), runtime, test.
  • Scope: compile sì nel war; provided compila ma non va nel war; runtime solo a esecuzione; test solo per i test.
xml
<dependency>
  <groupId>jakarta.servlet</groupId>
  <artifactId>jakarta.servlet-api</artifactId>
  <version>6.0.0</version>
  <scope>provided</scope>
</dependency>
  • Ciclo di vita: validate, compile, test, package, verify, install, deploy. Ogni fase esegue prima le precedenti. Le fasi sono collegate a obiettivi di plugin (compiler:compile, surefire:test, war:war).
bash
$ mvn clean package        # produce target/aule.war
$ mvn dependency:tree

Docker. Immagine = modello di sola lettura a strati, costruito da un Dockerfile; container = istanza in esecuzione e isolata, ciò che scrive si perde se non è in un volume.

dockerfile
FROM maven:3.9-eclipse-temurin-21 AS build
COPY pom.xml .
RUN mvn -q dependency:go-offline
COPY src ./src
RUN mvn -q package -DskipTests
FROM tomcat:10.1-jdk21
COPY --from=build /app/target/aule.war /usr/local/tomcat/webapps/ROOT.war
  • Istruzioni del Dockerfile: FROM (immagine di partenza, AS nome per la fase), WORKDIR, COPY (--from=build prende dalla fase precedente), RUN (esegue e salva uno strato), ADD (anche da URL), EXPOSE (documenta la porta).
  • Il driver di PostgreSQL va in /usr/local/tomcat/lib/, dove lo trova il pool di connessioni del container.
  • Due fasi: Maven compila, l'immagine finale ha solo Tomcat e il war. Il pom si copia prima del codice per riusare lo strato delle dipendenze. ROOT.war pubblica alla radice.
  • docker build -t aule-web ., docker run -p 8080:8080 aule-web; -p computer:container pubblica la porta.
  • compose: servizi db (PostgreSQL con POSTGRES_*, schema.sql in /docker-entrypoint-initdb.d/, volume dati, healthcheck pg_isready) e web (build: ., depends_on: service_healthy). Host del database nella rete: db. Lo script iniziale gira solo a volume vuoto: docker compose down -v per ripartire.
  • docker compose up --build -d, ps, logs -f web, down.
yaml
services:
  db:
    image: postgres:16
    environment: { POSTGRES_DB: aule, POSTGRES_USER: aule, POSTGRES_PASSWORD: aule }
    volumes:
      - dati:/var/lib/postgresql/data
      - ./sql/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
  web:
    build: .
    ports: ["8080:8080"]
    depends_on: { db: { condition: service_healthy } }
volumes:
  dati:
  • Push rifiutato: ! [rejected] main -> main (fetch first); si risolve con git pull, poi git push.
  • git fetch aggiorna solo origin/main; git pull fa anche il merge nel ramo corrente.

Errori tipici

  • Committare target/, .war o password.
  • Lavorare su main in gruppo; dare per corretto un merge senza conflitti senza compilare.
  • API servlet con scope compile: due copie delle classi nel war.
  • Confondere immagine e container; dimenticare -p; usare localhost come host del database dentro il container.

Domande d'esame (tracce)

  • Fast-forward contro commit di merge: antenato diretto o storie divergenti. Conflitto: stesse righe, marcatori, add, commit.
  • mvn package: validate, compile, test, package; provided perché la libreria la dà Tomcat.
  • Immagine e container; Dockerfile a due fasi; compose con rete (db), volume e script di inizializzazione.

Collegamenti: Architettura delle applicazioni web - browser, server web e livelliUn'applicazione web è distribuita su più livelli (tier): presentazione (browser: HTML, CSS, JavaScript), logica applicativa (server web e application server) e dati (DBMS). Il client invia richieste HTTP, il server risponde; HTTP è senza stato, quindi lo stato dell'utente viaggia in cookie e sessioni. Il web server serve file statici, l'application server (container, per esempio Tomcat per le servlet Java) esegue codice che genera risposte dinamiche; spesso il web server fa da reverse proxy davanti all'application server. Il browser trasforma i byte della risposta in una pagina: decodifica, tokenizzazione e costruzione del DOM, CSS che diventa CSSOM, render tree, layout (reflow), paint e composizione; un script senza defer o async blocca il parsing, il CSS blocca il rendering. Origine = schema + host + porta; la same-origin policy limita ciò che uno script può leggere da altre origini. Web 1.0: pagine generate dal server, ogni azione ricarica la pagina; Web 2.0: AJAX, contenuti degli utenti, servizi REST; Web 3.0: dati con significato esplicito (RDF, linked data).Architettura delle applicazioni web - browser, server web e livelli →, Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioniUna servlet è una classe Java che un container (Apache Tomcat: connettore HTTP Coyote, contenitore Catalina, compilatore JSP Jasper) istanzia una sola volta e chiama per ogni richiesta, ciascuna in un thread del pool. Ciclo di vita: caricamento e costruzione, init() una volta, service() per ogni richiesta (HttpServlet lo smista su doGet, doPost, doPut, doDelete), destroy() allo spegnimento. La richiesta (HttpServletRequest) dà metodo, parametri (query string e corpo del form), header, path (contextPath, servletPath, pathInfo) e corpo; la risposta (HttpServletResponse) si costruisce con stato, header e corpo (PrintWriter), con sendRedirect (302 + Location) o sendError. Il mapping si dichiara con @WebServlet o in WEB-INF/web.xml (che prevale). Poiché esiste una sola istanza e molti thread, i campi di istanza sono condivisi: lo stato della richiesta sta in variabili locali, quello condiviso in oggetti thread-safe (AtomicInteger, DataSource). HTTP è senza stato, quindi si usano le sessioni: la servlet crea un HttpSession, tiene i dati sul server e manda al browser solo l'identificatore nel cookie JSESSIONID (Set-Cookie con HttpOnly, Secure, SameSite, Path secondo RFC 6265); al login si cambia l'identificatore (changeSessionId) contro la session fixation. I filtri (Filter, FilterChain) elaborano richiesta e risposta prima e dopo le servlet: codifica, autenticazione, log. Il war ha la struttura WEB-INF/classes, WEB-INF/lib, WEB-INF/web.xml, META-INF/context.xml; l'accesso ai dati usa un DataSource con pool di connessioni dichiarato in context.xml e cercato con JNDI, e un DAO con PreparedStatement e try-with-resources.Java servlet e Apache Tomcat - ciclo di vita, richieste e sessioni →, Esercizio - Git con branch, merge e conflitto (applicazione web).

Esercizi su questo argomento

Teoria collegata