mattone
dopo mattoneLA COLLANA
IT/EN
← Guida Java

Java 25 · 37/39

Attività guidate per C29 e C30

Java 25 · Guida completa · Bozza in revisione

Questa guida conserva lo stato di revisione del libro. La verifica editoriale e le prove di comprensione con lettori indipendenti sono ancora da completare.

Cerca in tutta la guida →

Queste attività usano il catalogo del capitolo 29 e i programmi del capitolo 30. Ogni modifica parte da un difetto osservabile: prima si formula una previsione, poi si cambia una cosa e si controlla il risultato. I comandi che producono file scrivono in target/ o in una cartella build/ locale. Per C29 servono JDK 25 e Maven con accesso alle dipendenze dichiarate nel POM; per C30 basta il JDK 25.

C29.1 – Il JAR esiste, ma non parte

Un collega riceve catalogo-1.0.0.jar. jar --list --file target/catalogo-1.0.0.jar mostra le classi, mentre java -jar risponde che manca l'attributo principale del manifest. Prima di modificare il codice Java, individua il confine che ha fallito. Ricostruisci una versione del JAR senza la configurazione mainClass, osserva il problema, ripristina il POM ed esegui mvn clean verify e il JAR con dati/titoli.txt.

Criterio di riuscita. Il lettore sa indicare il file META-INF/MANIFEST.MF, mostra la voce Main-Class: esempio.catalogo.CatalogoCli nel JAR corretto e ottiene due titoli in output. Sa spiegare perché la presenza di CatalogoCli.class non basta a java -jar per scegliere il punto d'ingresso.

Indizio. Confronta jar --describe-module --file ... solo se stai studiando i moduli; per questo esercizio apri il manifest con unzip -p target/catalogo-1.0.0.jar META-INF/MANIFEST.MF oppure con un programma per archivi. L'applicazione usa il class path.

Soluzione ragionata. java -jar legge il manifest. Il plugin JAR del POM scrive Main-Class perché la configurazione <mainClass> lo richiede. Senza quella voce il JAR può contenere classi compilate ed essere comunque privo di un ingresso scelto per questa modalità di avvio. La riparazione è nella build; cambiare main non risolve il difetto dell'artefatto.

C29.2 – Un test verde non copre il limite

Il catalogo accetta un titolo di ottanta unità char e rifiuta quello di ottantuno. Aggiungi un test per il primo caso e verifica il secondo. Prima di eseguirlo, scrivi quale risultato attendi per "x".repeat(80) e "x".repeat(81). Cambia temporaneamente la condizione di Titoli.normalizza da > 80 a >= 80: il test nuovo deve diventare rosso. Ripristina il codice e ripeti mvn clean verify.

Criterio di riuscita. Il rapporto Surefire mostra il nuovo test; la mutazione >= 80 viene scoperta; il codice ripristinato torna verde. Il lettore sa dire che una percentuale di copertura, anche alta, non avrebbe espresso da sola questo confine.

Indizio. Usa assertEquals(80, Titoli.normalizza("x".repeat(80)).length()) e conserva assertThrows per l'input da 81. Non modificare il test per adattarlo al difetto introdotto.

Soluzione ragionata. La regola è inclusiva sul massimo: length() > 80 rifiuta soltanto lunghezze superiori. Il test a 80 verifica il lato ammesso, quello a 81 il lato rifiutato. Una mutazione che sposta il confine deve far fallire almeno il primo. Questo è più informativo di un test che esegue semplicemente il metodo.

C29.3 – Chi usa JUnit quando parte l'applicazione?

Esamina pom.xml, poi esegui mvn dependency:tree se il plugin è disponibile nel tuo ambiente. Individua JUnit e le eventuali sue dipendenze transitive. Produci il JAR, controlla che non contenga classi org/junit, ed esegui l'applicazione. Formula la conseguenza di sostituire <scope>test</scope> con il comportamento predefinito, senza sostenere che Maven copierà automaticamente tutte le librerie dentro il JAR ordinario.

Criterio di riuscita. Il lettore distingue dipendenza di test, dipendenza di runtime e contenuto fisico del JAR. Riconosce che il plugin JAR usato qui non crea un fat JAR.

Indizio. jar --list --file target/catalogo-1.0.0.jar mostra il contenuto effettivo. dependency:tree mostra invece il grafo risolto dalla build: sono osservazioni diverse.

Soluzione ragionata. JUnit è necessario per compilare ed eseguire i test. Lo scope di test lo esclude dal normale class path dell'applicazione. Cambiare lo scope può allargare il grafo delle dipendenze dell'applicazione, ma non trasforma da solo il JAR ordinario in un archivio che incorpora i JAR esterni. Il programma funziona perché le tre classi di produzione usano soltanto API del JDK.

C29.4 – Dal JAR a un altro ambiente

Esegui jdeps --print-module-deps target/catalogo-1.0.0.jar; crea una runtime image con jlink --add-modules java.base --output target/runtime-catalogo e avvia il JAR con target/runtime-catalogo/bin/java -jar .... Poi aggiungi, su un ramo o in una copia di prova, una classe che usa direttamente jdk.jfr.Recording. Ripeti l'analisi e spiega perché un runtime costruito solo con java.base non soddisfa più il nuovo programma. Ripristina il progetto del capitolo.

Criterio di riuscita. Il lettore mostra l'output di jdeps prima e dopo la modifica, collega jdk.jfr alla nuova API e non scambia la dimensione del runtime per una prova funzionale. Il trasferimento può essere spiegato anche senza produrre il secondo pacchetto.

Indizio. jdeps analizza dipendenze statiche. Una riflessione o un caricamento dinamico può richiedere controlli ulteriori durante l'esecuzione.

Soluzione ragionata. L'applicazione iniziale usa API di java.base; quindi quel modulo basta al runtime provato. La classe aggiunta importa jdk.jfr.Recording, e il grafo statico deve includere jdk.jfr. L'analisi suggerisce i moduli per jlink, ma l'avvio e i casi d'uso restano prove necessarie, specialmente quando il programma carica componenti in modo dinamico.

C30.1 – Il limite è un comportamento, non un commento

Compila ImportazioneControllata.java e ProvaImportazioneControllata.java con javac --release 25 -Xlint:all -d build ... nella cartella C30. Aggiungi due prove: una riga di esattamente 80 unità char deve essere accettata; un archivio con esattamente 100 righe non vuote deve essere accettato. Mantieni i casi da 81 e 101 già presenti, che devono essere rifiutati. Spiega che cosa succede se il carattere finale è \r\n.

Criterio di riuscita. Quattro lati dei due confini hanno un risultato atteso esplicito. Il lettore sa localizzare il controllo prima dell'accumulo della riga e prima dell'aggiunta del titolo numero 101.

Indizio. "x".repeat(80) e "x\n".repeat(100) costruiscono input senza file esterni. Il parser ignora \r e usa \n come fine riga; questa è una scelta del formato didattico, da documentare se i dati hanno altre convenzioni.

Soluzione ragionata. Quando riga.length() vale 80, il carattere successivo diverso da fine riga causa IOException; una riga di 80 entra, quella di 81 no. Quando la lista contiene 100 titoli, la successiva chiamata ad aggiungi fallisce. La lettura di \r\n non aggiunge \r al titolo. Il test verifica il contratto concreto, non una vaga promessa di «input controllato».

Variante. Prova a inviare molte righe vuote. Il limite di cento titoli non interrompe questa lettura: decidi un massimo complessivo di caratteri o righe, aggiungi un errore distinto e una prova che lo attraversi. Il nuovo controllo va fatto durante la lettura, non dopo averla completata.

C30.2 – Un percorso fornito dall'utente

Immagina di esporre il catalogo come servizio: il client invia il nome del file da importare. Progetta una directory autorizzata e scrivi due casi di prova, uno per un file interno e uno per un percorso che tenta di uscire con ... Aggiungi un terzo caso con un collegamento simbolico che punta fuori dalla directory. Descrivi dove va collocato il controllo e quale rischio resta se il filesystem cambia tra controllo e apertura.

Criterio di riuscita. La soluzione non si limita a cercare la sottostringa ..; considera normalizzazione, link simbolici, autorizzazione e apertura del file. Distingue inoltre il limite di righe del parser dal permesso di accedere a quel file.

Indizio. Parti dalla directory radice decisa dal server, non dalla directory corrente del processo. Le API Path, Files e gli attributi del filesystem aiutano a ragionare sul percorso effettivo; in alcuni sistemi serve una strategia di apertura che riduca la finestra tra controllo e uso.

Soluzione ragionata. Il client non deve scegliere liberamente qualunque percorso del processo. La soluzione definisce una radice, risolve e verifica il candidato rispetto a essa e tratta i link simbolici secondo una politica esplicita. Il controllo del nome e quello dell'oggetto effettivamente aperto non sono identici: un attaccante con accesso concorrente al filesystem può cambiare l'oggetto dopo una verifica separata. Il progetto del servizio deve scegliere API e permessi adeguati al sistema operativo, poi provare il percorso consentito e quelli rifiutati.

C30.3 – Log e registrazione raccontano fatti diversi

Esegui DiagnosiImportazione e stampa gli eventi con jfr print --events esempio.catalogo.Importazione build/catalogo.jfr. Confronta il messaggio Importati 2 titoli del logger con un evento JFR. Per ciascuno annota la domanda a cui risponde e una domanda a cui non risponde. Proponi un identificatore di richiesta che colleghi un errore di importazione ai log del servizio senza scrivere i titoli ricevuti.

Criterio di riuscita. Sono visibili tre eventi, ciascuno con titoli = 2; le durate vengono trattate come osservazioni locali. Il lettore evita di mettere input, token o dati personali nei campi diagnostici per comodità.

Indizio. Il log dice che una chiamata è terminata con un conteggio. L'evento JFR, se abilitato nel periodo giusto, conserva anche un intervallo temporale. Nessuno dei due prova che l'input di una richiesta diversa sarebbe accettato.

Soluzione ragionata. Il log è un messaggio applicativo su un risultato. L'evento JFR collega il tipo di operazione, il conteggio, il thread e una durata osservata. Un identificatore opaco generato al confine della richiesta può apparire nei log di ingresso, errore e uscita; deve avere una politica di conservazione e non deve rivelare il contenuto del file. Per investigare lentezze intermittenti servono misure nel periodo del problema, non tre numeri dell'esempio.

C30.4 – Trasferire il modello a HTTP e database

Ora il catalogo scarica titoli da una risposta HTTP e li salva in un database. Scrivi un contratto operativo per il caso in cui la risposta si interrompa a metà e per quello in cui il database fallisca dopo 40 inserimenti. Indica dove limiti dimensione e tempo della risposta, quando convalidi i record, come eviti uno stato parziale e che cosa osservi durante un nuovo tentativo. Non occorre scegliere un framework: la prova riguarda le decisioni e i confini.

Criterio di riuscita. La soluzione collega un problema a ogni misura: limiti di rete contro risposte senza fine; validazione contro dati malformati; transazione o strategia equivalente contro importazioni parziali; idempotenza contro duplicati nei retry; log e metriche per conoscere l'esito senza esporre dati. Il lettore indica almeno un test che provoca ciascun guasto.

Indizio. La lista del parser può essere valida e il salvataggio fallire comunque. Chiediti quando il nuovo catalogo debba diventare visibile agli utenti.

Soluzione ragionata. Si delimita la risposta con timeout e massimo di byte o record. Si verifica il formato mentre si legge e si rifiuta l'intera importazione se la politica richiede un insieme completo. Il salvataggio avviene in una transazione, oppure in una nuova versione sostituita solo al termine: così il fallimento dopo 40 righe non espone un catalogo incompleto. Un identificatore dell'importazione o una chiave naturale permette di decidere l'effetto di un retry. Test con risposta troncata, record errato e fallimento del database dimostrano il comportamento; i log mostrano fase, esito e identificatore senza copiare i titoli.

Prova gli esempi

Per eseguire i programmi serve JDK 25. Puoi scaricare i singoli file Java collegati nel capitolo oppure il progetto completo, che contiene istruzioni e uno script di avvio. Le spiegazioni confrontano anche l’output atteso: prevedilo prima di eseguire il programma.

Massimiliano Tarquini · CC BY-NC 4.0

Torna all’inizio ↑