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
$ 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:
$ 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).
$ 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:
$ 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-labTra <<<<<<< HEAD e ======= c'è la versione del ramo corrente; tra ======= e >>>>>>> rinomina-lab quella del ramo che si sta fondendo. Si risolve così:
- 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;
git add schema.sql: dice a git "risolto";git commit(il messaggio di merge è già pronto,--no-editlo 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:
$ 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:
maincontiene sempre codice che compila e funziona; non ci si committa direttamente;- per ogni funzione o correzione:
git switch -c nome-funzioneda unmainaggiornato (git pull); - commit piccoli e frequenti sul ramo; ogni tanto
git pull origin mainper portarsi dietro le novità ed evitare conflitti enormi alla fine; - 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; - 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 .gitignoreIl POM
<?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:
- Le coordinate
groupId:artifactId:versionidentificano in modo unico un artefatto:it.unipd.aule:aule:1.0-SNAPSHOT.SNAPSHOTindica una versione in sviluppo, che può cambiare. packagingwardice a Maven di produrre un Web ARchive (un file zip con struttura fissa, vedi 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 →) invece di unjar.maven.compiler.releasefissa la versione di Java (21); la codifica UTF-8 evita che le lettere accentate cambino da un computer all'altro.- Ogni
dependencyè una libreria identificata dalle sue coordinate. Maven la scarica con le sue dipendenze transitive (le librerie di cui essa stessa ha bisogno) dal repository remoto (Maven Central) e la tiene nel repository locale (cartella.m2/repositorynella home), così non la riscarica. - Lo scope dice quando la dipendenza serve:
| 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/.
$ 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 transitiveIl 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-jdk21significa "immaginetomcat, 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.
# --- 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.
$ 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.
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
dbusa l'immagine ufficiale di PostgreSQL, che al primo avvio crea database e utente indicati dalle variabili d'ambientePOSTGRES_*, ed esegue gli script SQL della cartella/docker-entrypoint-initdb.d/: montando lìsql/schema.sqlil 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.xmldi Tomcat l'URL JDBC èjdbc:postgresql://db:5432/aule, condbcome host. depends_onconservice_healthyfa partire Tomcat solo dopo che PostgreSQL risponde (pg_isready); altrimenti il pool di connessioni troverebbe il database non ancora pronto.build: .costruisce l'immaginewebcon il Dockerfile di prima;portspubblica la 8080.
$ 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.waro password nel repository. - Lavorare direttamente su
mainin 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 --forceogit reset --hardsu 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
localhostcome host del database nelcontext.xml: dentro il container di Tomcatlocalhostè 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 è.
$ 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 ramogit 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:
mainnon è 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 --abortper rinunciare. - Remoto
origin:clone,fetch(non tocca i rami),pull(= fetch + merge),push(la prima volta-u origin main). Pushrejected (fetch first): primagit pull, poi di nuovopush. Mai--forcesu 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:
mainsempre funzionante, un ramo per funzione, commit piccoli,pullfrequenti, 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 epackaging,properties(maven.compiler.release,project.build.sourceEncoding),dependencies,build(finalNameaule, pluginmaven-war-plugin). - Coordinate
groupId:artifactId:version(it.unipd.aule:aule:1.0-SNAPSHOT),packagingwar,maven.compiler.release21. - 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:
compilesì nel war;providedcompila ma non va nel war;runtimesolo a esecuzione;testsolo per i test.
<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).
$ mvn clean package # produce target/aule.war
$ mvn dependency:treeDocker. 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.
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 nomeper la fase),WORKDIR,COPY(--from=buildprende 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.warpubblica alla radice. docker build -t aule-web .,docker run -p 8080:8080 aule-web;-p computer:containerpubblica la porta.- compose: servizi
db(PostgreSQL conPOSTGRES_*,schema.sqlin/docker-entrypoint-initdb.d/, volumedati, healthcheckpg_isready) eweb(build: .,depends_on: service_healthy). Host del database nella rete:db. Lo script iniziale gira solo a volume vuoto:docker compose down -vper ripartire. docker compose up --build -d,ps,logs -f web,down.
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 congit pull, poigit push. git fetchaggiorna soloorigin/main;git pullfa anche il merge nel ramo corrente.
Errori tipici
- Committare
target/,.waro password. - Lavorare su
mainin 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; usarelocalhostcome 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;providedperché 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).