Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Luca Sau
16 dicembre 2022
Indice
I Back-End 5
1 Java 6
1.1 JRE vs JDK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2 Principi dell’ O.O.P. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.1 Astrazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.2 Ereditarietà . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2.3 Polimorfismo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.2.4 Incapsulamento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3 Classi astratte e Interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.1 Classi Astratte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.2 Interfacce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4 Eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.4.1 Gerarchia delle eccezioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.4.2 Gestione delle eccezioni da parte della JVM . . . . . . . . . . . . . . . . . . 18
1.4.3 User Defined Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.5 Java Collections e Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.5.1 Collection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.5.2 Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.6 Stream e Programamzione Funzionale . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.6.1 Lambda Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.6.2 Method References . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.6.3 Java Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2 Database 29
2.1 DB Relazionali o SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.2 DB Non Relazionali o NO-SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.3 SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.3.1 Join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.3.2 Relazioni tra tabelle: Foreign Key . . . . . . . . . . . . . . . . . . . . . . . 32
2.3.3 Tipi di relazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3 Spring 34
3.1 Inversion Of Control e Dependency Injection . . . . . . . . . . . . . . . . . . . . . 34
3.1.1 IoC Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.2 Crezione dei bean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.3 Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.4 Moduli di Spring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.5 Spring Boot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.6 Annotazioni Utili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.6.1 Specializzazioni di @Component . . . . . . . . . . . . . . . . . . . . . . . . 38
3.6.2 Annotazioni di Spring Web . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
3.6.3 Annotazioni per le entità della JPA . . . . . . . . . . . . . . . . . . . . . . 41
3.6.4 @Transactional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.6.5 Annotazioni di Lombok . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.6.6 @Mapper . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
3.7 Hibernate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
1
4 Rest API 46
4.1 REST . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.2 Metodi HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.3 Standard d’Industria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.4 REST VS SOAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5 Microservizi 48
5.1 Architettura monolitica vs. architettura di microservizi . . . . . . . . . . . . . . . 48
5.2 Caractteristiche dei Microservizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.3 Vantaggi dei Microservizi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.4 Microservizi in Spring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
II Front-End 60
7 TypeScript 61
7.1 Javascript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.1.1 JS Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.1.2 Ereditarietà in JS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.1.3 Costruttori e funzioni costruttore . . . . . . . . . . . . . . . . . . . . . . . . 63
7.1.4 Closure e Lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
7.1.5 Scope delle variabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
7.1.6 Classi in JS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
7.2 TypeScript . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7.2.1 Tipi in TS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7.2.2 Tipi comuni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8 HTML e CSS 71
8.1 HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.1.1 Struttura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.1.2 Elementi HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.1.3 Attributi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.1.4 Tabelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.1.5 Liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
8.2 CSS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
8.2.1 Selettori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
8.2.2 Dichiarazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
8.2.3 Position . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
8.2.4 FlexBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
9 Angular 81
9.1 Moduli . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
9.2 Componenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
9.2.1 Comunicazione tra componenti . . . . . . . . . . . . . . . . . . . . . . . . . 84
9.3 Template e Data Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
9.3.1 Interpolazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
9.3.2 Data Binding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
9.4 Direttive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
9.5 Dependency Injection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
9.6 Collegamento col Back-End . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
9.6.1 Observable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
9.6.2 Utilizzo delle chiamate HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . 91
9.7 Argomenti aggiuntivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
9.7.1 Sincronia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
9.7.2 Caricamento dei dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
2
9.7.3 File di configurazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
3
Introduzione
Blablabla
Per segnalazioni su imprecisioni/suggerimenti scrivete pure a l.sau@contrader.it o su discord Ral6h#7092.
4
Parte I
Back-End
5
Capitolo 1
Java
NOTA BENE: la JDK è necessaria non solamente quando si sviluppa; ad esempio, se si de-
ploya una web-app che utilizza le JSP, in quanto l’ application server deve convertire le JSP in
Java Servlets e ha bisogno del compilatore per farlo!
6
1.2 Principi dell’ O.O.P.
I quattro principi fondamentali della programmazione orientata a oggetti sono:
1. Astrazione
2. Ereditarietà
3. Polimorfismo
4. Incapsulamento
Seguire questi principi garantisce la stesura di un codice flessibile, facilmente testabile e coerente
con gli standard. Vediamo ora tali principi nel dettaglio e come rispettarli quando scriviamo in
Java.
1.2.1 Astrazione
L’astrazione è un principio cardine del design di un applicazione: date le dimensioni spesso molto
grandi di un programma orientato a oggetti, è impensabile aspettarsi che gli utilizzatori diretti
debbano essere a conoscenza del funzionamento interno di ogni classe. Di conseguenza, è una prio-
rità fornire un livello di astrazione, in modo che il programma sia semplice sia da utilizzare che da
comprendere.
In Java ciò viene realizzato tramite l’utilizzo di interfacce e classi astratte, le quali ci permet-
tono di dichiarare quali sono le funzionalità offerte da un oggetto che implementi/concretizzi un
tale contratto, senza tuttavia svelare come tali funzionalità vengano di fatto realizzate.
Vedremo nel dettaglio le differenze tra iùun’interfaccia e una classe astratta nella prossima
sezione.
1.2.2 Ereditarietà
L’ereditarietà ci consente di strutturare una gerarchia tra le classi. Una classe figlia eredita tutte le
funzionalità della classe madre, ciò consente di strutturare gerarchicamente il nostro programma,
evitando ripetizioni di codice, consentendo maggior chiarezza e specializzazione.
In java, per creare una classe figlia di un’altra si utilizza la keyword extends nella dichiarazione.
Diamo innanzitutto un’occhiata al codice:
7
import java.util.Date;
myImp.greetings();
myImp.demoSuper();
}
}
//output: "Ciao!"
// "di Impiegato / di Persona"
• Persona è la classe madre, essa ha come valori un nome e una data di nascita. Il costruttore
assegna questi valori. Oss: abbiamo volutamente chiamato i parametri del costruttore con lo
stesso nome delle variabili locali; questo non è un problema in quanto grazie alla keyword this
siamo in grado di specificare quando ci stiamo riferendo alle variabili e quando ai parametri.
• Impiegato è la classe figlia: eredita tutti gli attributi e i metodi di Persona, senza alcun
bisogno di dichiararli nuovamente, e introduce l’attributo id, specializzandosi in questo ri-
spetto alla classe madre. Osserviamo ora bene il costruttore: la keyword super invoca il
costruttore della classe madre, inizializzando quindi le variabili ereditate. Oss: super ha
anche un altro utilizzo, simile a quello di this: serve a riferirsi a una variabile della classe
madre qualora essa venga nascosta nella classe figlia, come nel caso di CONST nell’esempio
di qui sopra.
È importante ricordare che una classe può estendere una e una sola classe madre. Inoltre è
possibile limitare la disponibilita all’ereditarietà tramite la keyword final: una classe o un metodo
dichiarato come final non può essere estesa/o (mentre una variabile, al solito, non può essere
modificata).
8
Classi astratte e Interfacce
Vediamo ora come sono legate classi astratte e interfacce col concetto di ereditarietà.
Una classe astratta si comporta per lo più come una classe concreta, la principale differenza è che
una classe figlia deve (per forza di cose) implementare tutti i metodi dichiarati astrattamente nella
classe madre. Anche in questo caso una classe può estendere una e una sola classe astratta.
DOMANDA Ha senso dichiarare una classe astratta come final?
Per quanto riguarda le interfacce, esse non vengono estese bensı̀ implementate, in quanto esse
sono concettualmente un contratto che la classe implementante deve rispettare. Le interfacce
stanno quindi a metà tra il principio di ereditarietà e quello di polimorfismo. A differenza di una
classe astratta, nelle interfacce tutte le variabili sono (implicitamente) dichiarate come static e
final, di conseguenza una classe che implementa un’interfaccia può accedere a tali variabili ma non
modificarle. Per quanto riguarda i metodi, essi sono implicitamente public e astratti (solamente
dichiarati) e devono essere implementati dalla classe implementante. Ci sono tuttavia casi in cui
uno stesso metodo sia pensato per svolgere sempre la stessa funzione indipendentemente dalla
classe che implementa l’interfaccia; per questo motivo, da Java 8 in poi, nelle interfacce è possibile
scrivere dei metodi espliciti tramite la keyword default. Infine, bisogna ricordare che una classe
può implementare più di una interfaccia.
1.2.3 Polimorfismo
Il principio del polimorfismo può essere brevemente riassunto con la frase ”stesso nome, più forme”.
Messa semplicemente, il polimorfismo consente di svolgere una stessa azione in più modi differenti.
In java questo si esplicita principalmente in tre concetti:
1. Overload di Metodo
2. Override di Metodo
3. Java Generics
Vediamoli nel dettaglio:
Overload
L’overload consiste nel sovracaricare un metodo, ovvero nel dichiarare più metodi che abbiano lo
stesso nome ma type-signature differente. Java distingue questi metodi in base agli argomenti a
compile time. Vediamo un esempio:
import java.util.Date;
9
In questo caso sia il costruttore che il metodo greetings() sono overloaded. NOTA BENE: affinchè
Java possa capire a quale dei metodi fare riferimento, è necessario che questi abbiano argomenti
di tipi diversi, altrimenti si avrà un errore di compilazione.
Override
L’override consiste nel sovrascrivere un metodo già implementato in una classe madre, mantenendo
la stessa type signature. A differenza dell’overload, in questo caso Java capisce a runtime quale
metodo chiamare in base alla dichiarazione dell’oggetto. Veidamo un esempio:
import java.util.Date;
@Override
public void print() {
System.out.println("Impiegato");
}
}
}
}
Generics
I generics permettono di passare il tipo di dati come argomento a metodi, classi e interfacce.
Usando i generici, è possibile creare classi che svolgano le stesse operazioni con diversi tipi di dati
senza dover ripetere codice. Una tale classe (/interfaccia/metodo) viene detta generica. Vediamo
nel dettaglio di cosa si tratta:
10
}
}
// Utilizziamola
class Main {
public static void main(String[] args)
{
// Creiamo un’instanza di Test con tipo Integer
Test<Integer> iObj = new Test<Integer>(15);
System.out.println(iObj.getObject());
//output: 15
// Contrader
La classe Test ha le stesse funzionalità sia che obj sia una string o un intero (o di qualsiasi altro
tipo), se non avessimo utilizzato i generici avremmo dovuto scrivere due (o più) classi diverse a
seconda del tipo desiderato!
class Main
{
public static void main (String[] args)
{
Test <String, Integer> obj =
new Test<String, Integer>("Contrader", 15);
obj.print();
}
}
//output: Contrader
// 15
11
class Test {
// Esempio di metodo generico
static <T> void genericDisplay(T element)
{
//Stampa tipo e valore del parametro
System.out.println(element.getClass().getName()
+ " = " + element);
}
// java.lang.Integer = 11
// java.lang.String = Contrader
// java.lang.Double = 1.0
1.2.4 Incapsulamento
Per incapsulamento si intende il racchiudere un insieme di dati all’interno di una singola unità.
Alternativamente, si può pensare all’incapsulamento come a un ’guscio protettivo’ che previene
l’accesso diretto ai dati all’esterno del guscio stesso.
Tecnicamente, nell’incapsulamento, le variabili di una classe sono nascoste alle altre classi e l’accesso
a queste è possibile solamente attraverso dei metodi della classe guscio. Questo concetto è detto
anche data-hidding e in Java è realizzabile in Java tramite l’utilizzo della keyword private nella
dichiarazione della variabile o del metodo che si vuole incapsulare. tale keyword fa sı̀ che solamente
gli altri elementi della classe possano accedere direttamente alla proprietà, garantendo alle classi
esterne il corretto utilizzo della classe ma nascondendone l’effettiva struttura. In questo senso
l’incapsulamento si ricollega al principio di astrazione!
È best practice (i.e. fatelo!!!) dichiarare ogni variabile di una classe come private e gestirne
l’accesso esterno tramite i getter e i setter:
Ricordiamo infine che Java offre, oltre a private, altri tre modificatori d’accesso:
• public: se posto di accanto a una poprietà, questa è direttamente accessibile a una qualun-
que classe del progetto che importi la classe che la contiene. È ad esempio utilizzato nelle
dichiarazioni dei getter e setter.
12
• protected: può essere attribuito solo ai metodi e alle variabili interne alla classe e non può
essere applicato alla classe stessa. I metodi e le variabili dichiarate come protected sono
visibili da classi dichiarate nello stesso package e da sottoclassi e classi derivate dichiarate
ovunque.
• default: viene assegnato automaticamente dal compilatore solo quando nella scrittura del
sorgente si omettono gli altri modificatori. Di default indica che l’elemento è di accesso
pubblico ma solo ed esclusivamente al package della classe dove è inserito, diversamente da
public e protected dove è visibile anche in package esterni.
NOTA BENE: una classe può essere dichiarata private? Sı̀, ma solamente se è una classe interna,
ovvero una classe dichiarata all’interno di un’altra, che viene di fatto trattata come una proprietà
della classe esterna.
@Override
public void greeting() {
System.out.println("Wewe amici del webb!");
}
Come notiamo dall’esempio, una classe astratta può contenere sia metodi astratti che non,
tuttavia se vi è anche solo un metodo astratto l’intera classe deve essere dichiarata astratta. Inol-
tre una classe che estende una classe astratta deve o implementare tutti i metodi astratti (e.g.
greetings() sopra) oppure essere dichiarata astratta essa stessa.
13
1.3.2 Interfacce
Un interfaccia può essere pensata come uno schema che definisce un insieme di caratteristiche
richieste ma che non ne esplicita l’implementazione. Al lato pratico, per dichiarare un’interfaccia
si usa la keyword Interface. Una classe può implementare un’interfaccia tramite la keyword im-
plements, affinchè l’implementazione sia corretta deve inoltre esplicitare tutti i metodi astratti
che eredita dall’interfaccia stessa. A differenza delle classi astratte, una classe può implementare
più di un’interfaccia.
OSS: se si implementano due interfacce che contengono un metodo con stesso nome, stessa signa-
ture per gli argomenti ma diverso tipo di return si avrà un errore, in quanto java non sarà in grado
di capire quale dei due chiamare! Se invece i due metodi hanno argomenti diversi non avremo
problemi, in quanto basterà fare un overload del metodo all’interno della classe.
@Override
public String wewe(String nome) {
return "Wewe " + nome;
}
}
14
public Interface Animale {
void verso();
}
Per approfondire le domande precedenti si rimanda a questo link (dopo avere sbattuto la testa
sul codice!).
1.4 Eccezioni
Un’eccezione è un evento inaspettato che interferisce con la normale esecuzione di un programma.
Java offre al programmatore la possibilità di catturare (catch) tali eccezioni: quando una di queste
avviene durante l’esecuzione di un metodo, viene creata un’istanza della classe Exception. Tale
oggettto contiene informazioni riguardanti l’eccezione, come il nome, la descrizione di cosa è andato
storto e lo stato del programma al momento del lancio di tale eccezione.
Alcune delle ragioni più comuni per cui un’eccezione viene lanciata includono:
• input invalido da parte dell’utente
• malfunzionamento del dispositivo
• assenza di connessione alla rete
• memoria insufficiente
• errori nel codice
• tentare di aprire un file non disponibile
15
1.4.1 Gerarchia delle eccezioni
Vediamo del dettaglio come le eccezioni vengono formalizzate in Java. Tutte le eccezioni estondono
la classe di Throwable, questa classe è alla base della gerarchia e ha due classi figlie: Exception e
Error. La differenza principale tra queste due è che un’eccezione viene utilizzata per indicare un
avvenimento non ordinario che il programma deve essere in grado di gestire, mentre un errore viene
utilizzato dalla JVM per indicare un problema con la JRE stessa che non deve essere normalmente
gestito dal programma, come ad esempio uno stack overflow (che corrisponde al StackOverflowEr-
ror).
Java contiene una vasta gamma di eccezioni già pronte all’uso (built-in), tuttavia è possibile
per il programmatore definire una eccezione personalizzata qualora ci fosse bisogno; vedremo tra
un attimo come fare ciò, occupiamoci però prima di descrivere le eccezioni built-in.
Una built-in exception può essere checked o unchecked.
Unchecked Exceptions
Sono le eccezioni che non vengono controllate dal compilatore: esse vengono rilevate a runtime e il
programma compilerà anche se non vengono gestite esplicitamente. A livello di gerarchia, tutte e
sole le unchecked exception sono quelle che estendono la classe RuntimeException. Un esempio di
una tale eccezione avviene quando proviamo a dividere per 0, come nel seguente esempio:
class DemoExc {
public static void main(String args[]) {
int x = 0;
int y = 10;
int z = y / x;
}
}
Checked Exceptions
Sono le eccezioni che vengono controllate durante la compilazione. Se all’interno di un metodo
è presente del codice che potrebbe lanciare una o più di tali eccezioni, tale metodo deve o ge-
stirle internamente oppure nella dichiarazione deve essere utilizzata la keyword throws seguita
dalle eccezione che vengono lanciate. La gestione interna avviene tramite l’utilizzo di un blocco
try/catch/finally; la sintassi di tale blocco è la seguente:
16
public void myMethod() {
try{
//qui va inserita la porzione di codice da controllare, supponiamo
ad esempio che vengano lanciate Eccezione1, Eccezione2, ... ,
EccezioneN
}
catch(Eccezione1 e) {
//qui va inserito il codice da eseguire quando viene lanciata l’
Eccezione1
}
catch(Eccezione2 e) {
//qui va inserito il codice da eseguire quando viene lanciata l’
Eccezione2
}
.
.
.
catch(EccezioneN e) {
//qui va inserito il codice da eseguire quando viene lanciata l’
EccezioneN
}
finally {
//qui va inserito il codice da eseguire in ogni caso anche se viene
lanciata un’eccezione
}
}
Consideriamo ad esempio il seguente programma che apre il file a.txt e ne stampa le prime tre righe,
ma supponiamo di fornire un path sbagliato per tale file. Avremmo in questo caso errore di compi-
lazione, in quanto utilizziamo il metodo FileReader() che lancia FileNotFoundException. Notiamo
che utilizziamo anche i metodi readLine() e close(), i quali a loro volta lanciano IOException.
class Demo {
try {
FileReader file = new FileReader("C:\\LocationFake\\a.txt");
}
}
17
OUTPUT col metodo .printStackTrace():
java.io.FileNotFoundException: C:\LocationFake\a.txt (Impossibile trovare il
percorso specificato)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:111)
at java.base/java.io.FileReader.<init>(FileReader.java:60)
at it.demo.Demo.main(Demo.java:23)
Nel blocco finally
OSS: in alternativa avremmo potuto gestire il tutto tramite throws, in questo caso il metodo
utilizzato implicitamente per il display è il .printStackTrace()
class Demo {
public static void main(String[] args) throws IOException{
// cerco di leggere il file
FileReader file = new FileReader("C:\\LocationFake\\a.txt");
OUTPUT:
java.io.FileNotFoundException: C:\LocationFake\a.txt (Impossibile trovare il
percorso specificato)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:216)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:111)
at java.base/java.io.FileReader.<init>(FileReader.java:60)
at it.demo.Demo.main(Demo.java:23)
18
3. se trova un handler adatto (ad esempio un blocco try/catch coerente col tipo dell’eccezione)
passa ad esso l’eccezione.
4. altrimenti passa l’eccezione all’handler di default (interno alla JVM), il quale stampa lo
stacktrace e termina il programma.
Nel costruttore passiamo al costruttore di Exception il messaggio che vogliamo venga mostrato
quando questa eccezione viene lanciata.
Quando in un metodo vogliamo lanciare tale eccezione (che deve essere specificata nella dichiara-
zione del metodo) ne creiamo una nuova istanza e poi la lanciamo tramite la keywor throw (senza
la S!!!).
19
1.5.1 Collection
L’interfaccia Collection è dichiarata come
20
Metodi di Collection
Metodo Return Funzionamento
add(E e) boolean Aggiunge l’elemento e alla collection
addAll(Collection<? extends add(E e) Aggiunge alla collection tutti gli ele-
E> c) menti di c
clear() void Rimuove tutti gli elementi dalla collec-
tion
contains(Object o) boolean Restituisce true se o fa parte della col-
lection
containsAll(Collection< ? ex- boolean Restituisce true se c è una sottocolle-
tends E> c) zione della collezione data.
equals(Object o) boolean Compara la collection e o per vedere se
costituiscono ’effettivamente’ lo stesso
oggetto
hashCode() int Restituisce l’hashcode per la collection
isEmpty() boolean Restituisce true se la collezione non
contiene alcun elemento
iterator() Iterator<E> Restituisce l’iteratore un iteratore sugli
elementi della collection
(def.) parallelStream() Stream<E> Restituisce uno Stream parallelo che ha
la collection come fonte
remove(Object o) boolean Rimuove una singola istanza di o dalla
collection
removeAll(Collection<?> c) boolean Rimuove tutti gli elementi di c dalla col-
lection
(def.) removeIf (Predicate< ? boolean Rimuove gli elementi della collection
extends E> filter) che soddisfano filter
retainAll(Collection< ? ex- boolean Mantiene solamente gli elementi della
tends E> c) collection che stanno anche in c
size() int Restituisce il numero di elementi della
collection
(def.) splitIterator() SplitIterator<E> Crea uno SplitIterator sugli elementi
della collection
(def.) stream() Stream<E> Restituisce una stream che ha come fon-
te la collection
toArray() Object[ ] restituisce un array contenente gli ele-
menti della collection
toArray(T[ ] a) <T> T[ ] Restituisce un array di tipo T contenen-
te gli elementi della collection. OSS:
l’argomento a serve puramente per in-
dicare il tipo desiderato
Liste
List è un’interfaccia figlia di Collection. È dedicata alla gestione ordinata di oggetti, permettendo
la presenza di oggetti duplicati. Tale interfaccia è implementata da varie classi, come ad esempio
ArrayList e Vector. Poichè ciascuna di queste classi implementa List, possiamo istanziare una lista
utilizzando una qualsiasi di queste dichiarazioni:
21
• LinkedList: essa implementa la struttura dati lista (doppiamente) concatenata in java,
ovvero una struttura dati lineare nella quale gli oggeti sono salvati come nodi, ognuno dei
quali provvede un puntatore all’elemento successivo e uno a quello precedente.
• Vector: è identico ad ArrayList nell’implementazione, con la sola differenza di essere sincrono
(mentre ArrayList è asincrono).
• Stack: è una sottoclasse di Vector e impementa la struttura dati dello Stack, seguendo il
principio FILO (first in last out). Oltre ai classici metodi push e pop, essa provvede i tre
metodi aggiuntivi empty, search e peek.
Set
Set è un’interfaccia figlia di Collection, che rappresenta una collezione non ordinata di elementi non
duplicati. È particolarmente utile quando si vogliono memorizzare solamente valori unici evitando
ripetizioni. Vome per le liste, un Set è istanziabile creando un’istanza di una sua qualsiasi classe
implementante, come ad esempio:
while (myIter.hasNext()) {
System.out.println(myIter.next());
}
//OUTPUT: Wewe
// webbe
// amici del
SortedSet
È un’interfaccia che estende Set, fornendo metodi per la gestione dell’ordinamento degli elementi.
Viene implementata dalla classe TreeSet, che usa un albero per il salvataggio degli oggetti e
gestisce l’ordine o in base all’ordine naturale degli oggetti (che deve essere coerente col metodo
equals) oppure tramite un Comparator che può essere fornito come argomento del costruttore.
22
1.5.2 Map
Una mappa è una struttura dati che contiene delle coppie (chiave, valore). Le chiavi devono essere
uniche per evitare ambiguità, in quanto le chiavi vengono utilizzate per accedere al rispettivo valore.
Una mappa è utile quando si vogliono gestire dei dati complessi in base alle chiavi, tipcamente più
semplici. L’interfaccia è dichiarata come segue e una sua istanza può essere ottenuta tramite le
classi implementanti, ad esempio:
Interface Map<K,V>;
23
Le lambda sono inoltre utili in quanto permettono di snellire notevolmete la dichiarazione di una
funzione, consideriamo ad esempio il seguente codice, nel quale dichiariamo una stessa funzione in
due modi:
OSS: nella dichiarazione di myFunction abbiamo utilizzaato una classe anonima, che ci consente
di dichiarare e istanziare una classe allo stesso tempo, rendendo il codice più conciso. Per maggiori
dettagli, si rimanda a questo link.
2. Se abbiamo necessità di passare una Function come argomento di un metodo (ad esempio
nel .map() delle stream, come vedremo tra un attimo), possiamo utilizzare una lambda.
ClasseMadre::nomeDelMetodo
Le tramite le referenze di metodo ci consentono di referenziare sia metodi statici che non. In que-
st’ultimo caso ci dobbiamo assicurare che la classe madre sia istanziata in precedenza. Vediamo ora
un esempio volutamente artificioso per illustrarne il funzionamento; vedremo esempi più concreti
(e utili) con le stream.
test(2,Esempio::dimezza);
}
}
Nell’esempio di sopra, test prende come argomenti un input di tipo Integer e una funzione (adatta
all’input) e stampa il risultato della funzione applicata all’input. Quando chiamiamo test possiamo
quindi passare in input il metodo dimezza() tramite referenza di metodo.
24
1.6.3 Java Streams
Una stream è una sequenza di oggetti che supporta vari metodi, i quali possono essere composti
nella stream-pipeline per produrre il risultato desiderato. Le caratteristiche principali delle stream
sono:
1. non sono una struttura dati, bensı̀ vengono generate da una di queste, come ad esempio una
Collection o un Array. Di conseguenza le stream non possono essere usate per la gestione dei
dati, ma solamente per la manipolazione di dati già esistenti. È importante ricordare che una
stream non fornisce alcun accesso diretto ai dati in circolo (e.g. non esiste un’indicizzazione)
La sintassi per generare aprire una stream è la seguente:
• Nel caso di un array si utilizza invece il metodo statico .stream() della classe Arrays, il
quale prende in input un array e lo converte in una stream dello stesso tipo:
ATTENZIONE: in questo caso è importante che l’array non contenga tipi primiti-
vi, in quanto l’interfaccia Stream è generica e di conseguenza prende come parametro
un oggetto. È quindi buona norma usare i rispettivi type-wrappers quando si ha in
mente di adottare un approccio funzionale al codice.
2. Le stream non cambiano la struttura dati originale, forniscono solamente il risultato dei
metodi della pipeline.
Le operazioni applicabili alle stream si dividono in due macrogruppi:
1. Operazioni intermedie: restituiscono come output una stream dello stesso tipo di quella su cui
vengono applicate, possono quindi essere concatenate per spezzare un’operazione complessa
nella composizione di tante piccole operazioni
2. Operazioni terminali: restituiscono diversi (o nessun) output, di conseguenza chiudono la
stream.
È utile sapere che le operazioni intermedie sono lazy, ovvero non vengono eseguite finchè un’ope-
razione terminale non viene chiamata.
Vediamo ora le operazioni principali.
Operazioni intermedie
• .map(): prende in input una Function <T,V> dove T deve essere compatibile col tipo della
stream (ovvero deve essere o lo stesso tipo oppure una sua superclasse). Restituisce in output
una stream di tipo V (lo stesso tipo dell’ output della funzione), che ha in circolo il risultato
della function sugli elementi della stream iniziale.
Vediamo un esempio:
25
.[...]
System.out.println(myList2);
Fate attenzione all’utilizzo delle method reference e delle lambda come argomento della map!
(Nella prima map si è dovuto utilizzare Object come classe contenitore in quanto la referenza
Integer::toString è ambigua.)
• .filter(): prende in input un Predicate<T>, dove T è compatibile col tipo della strem e
restituisce in output una stream dello stesso tipo di quella iniziale, composta dagli elementi
che soddisfano il predicato (ovvero dali elementi che dati in input al predicato danno true in
output). Si ricorda che un predicato altro non è che una funzione a valori booleani. Di fatto
reduce viene quindi utilizzata per scremare i valori in base a una proprietà desiderata. Ad
esempio, supponiamo di voler fare la stessa operazione dell’esempio precedente ma solamente
sugli elementi pari della lista:
Anche in questo caso, essendo un predicato fondamentalmente una funzione, osserviamo come
l’utilizzo delle lambda ci consenta di scrivere un codice snello e facilmente leggibile.
Operazioni Terminali
• .collect() prende come input un Collector, ovvero un oggetto che si occupa di raccogliere
gli oggetti della stream in una particolare struttura dati. La classe Collectors contiene al
suo interno vari metodi statici che ci forniscono un tale collector, come ad esempio Collec-
tors.toList() e Collectors.toSet(). Per maggiori dettagli ri rimanda, al solito, alla documen-
tazione.
Vediamo quindi come possiamo utilizzare il collect per completare l’esempio precedente:
26
• .forEach() prende come argomento un Consumer<T>, dove T è compatibile col tipo della
stream, e ha void come output-type. Consumer è un’interfaccia funzionale che espone il
metodo .accept(), di conseguenza, anche in questo caso, possiamo usare sia delle lambda che
delle referenze di metodo come argomento del forEach(). Al lato pratico, il forEach() esegue
l’operazione specificata su ogni elemento della stream; supponiamo ad esempio, nell’esempio
precedente, di voler semplicemente stampare i risultati delle operzioni intermedie, senza
salvarli. Per farlo possiamo fare cosı̀:
//output: 2 Ciao
// 4 Ciao
• .reduce() prende come input un oggetto di tipo T, con T compatibile con il tipo della stream,
e un BinaryOperator<T>, ovvero una funzione che prende in inut due argomenti di tipo T
e restituisce in output un oggetto ancora di tipo T. (Nota: questa è una semplificazione per
scopi didattici. In realtà il metodo reduce è overloaded e ammatte altre due signature, per
approfondire si rimanda alla documentazione). Vediamo con mano come funziona: suppo-
niamo di voler ottenere la somma degli elementi di una stream di interi, possiamo utilizzare
reduce come segue
//output: 10
Smembriamo il codice: il primo argomento che passiamo al reduce indica il valore iniziale
che si vuole dare al risultato.
Poichè se la stream non contiene dati vogliamo ottenere 0 come somma, è sensato assegnare
proprio questo come valore iniziale.
Come secondo argomento abbiamo passato invece una lambda che prende due input (rispet-
tivamente ans e x) e aggiorna ans sommandoci x.
Quando il reduce viene chiamato, ans viene automaticamente inizializzato al valore passato
da noi come valore iniziale (in questo caso 0), in seguito l’operatore binario viene eseguito
tante volte quante sono gli elementi della stream. Nel nostro caso, aggiunge di volta in volta
gli elementi della stream ad ans, la quale alla fine dell’iterazione avrà come valore proprio la
somma.
E se avessimo voluto ottenere la somma degli elementi e poi sommarci uno? Semplice, basta
passare 1 come valore iniziale:
//output: 11
27
Per finire, vediamo come possiamo utilizzare il reduce per concatenare le stringhe dell’esempio
iniziale: (notare come in questo caso i valore iniziale sia la stringa vuota)
Consiglio: il miglior modo per imparare a fare qualsiasi cosa è sporcarsi le mani: questa era
solamente un’introduzione alla programmazione funzionale, integratela con una buona dose di
ricerche personali e soprattutto provate a aggiornare un codice già esistente sostituendo tutti i cicli
for presenti con delle stream equivalenti. Buona fortuna! :)
28
Capitolo 2
Database
Un databse è semplicemente un insieme di dati salvati su una macchina che possono essere con-
sultati in vari modi. In questo capitolo presenteremo i database relazionali e non, esaminando le
differenze tra i due.
• MySql
• MariaDB
• IBM DB2
• MongoDB
• Google Cloud Firestore
29
• Cassandra
• Redis
• Apache HBase
• Amazon DybamoDB
2.3 SQL
Concentriamoci ora sui database relazionali, rinfrescando innanzitutto le operazioni di base di SQL
tramite delle query esempio:
2.3.1 Join
Vediamo in particolare l’operazione JOIN. Questa operazione è utilizzata per combinare tra loro
dati appartenenti a due o più tabelle distinte in base a una proprietà (leggere ’colonna’) comune a
tutte quante. Un JOIN può essere dei seguenti tipi:
1. INNER JOIN
2. LEFT JOIN
3. RIGHT JOIN
4. FULL JOIN
Nel seguito seguiremo questo esempio (non avevo voglia di rifare le tabelle, scusate per i nomi non
esattamente facili :) )
Inner Join
Questa operazione utilizza la keyword INNER JOIN (o anche semplicemente JOIN, è equiva-
lente) per selezionare tutte le righe che soddisfano le condizioni specificate in entrambe le tabelle.
Il result set sarà quidni formato da tali righe.
Capiamo meglio con un esempio: supponiamo di avere le due tabelle Student e StudentCourse
Facciamo una inner join delle due tabelle, combinando la colonna ROLL NO di entrambe le tabelle.
30
22 SELECT StudentCourse.COURSE_ID, Student.NAME, Student.AGE FROM Student
23 INNER JOIN StudentCourse
24 ON Student.ROLL_NO = StudentCourse.ROLL_NO;
25 SELECT Student.NAME,StudentCourse.COURSE_ID
26 FROM Student
27 LEFT JOIN StudentCourse
28 ON StudentCourse.ROLL_NO = Student.ROLL_NO;
Notare come in corrispondenza di NIRAJ non ci sia un valore del COURSE ID, in quanto il suo
ROLL NO (8) non è presente nella tabella a destra.
Full Join
Questo join utilizza la keyword FULL JOIN per combinare di fatto un LEFT e un RIGHT JOIN.
Il result set sarà quindi formato da tutte le righe di tutte e due le tabelle, quando possibile (ovvero
quando la proprietà specificata matcha tra due righe) queste righe verranno combinate tra loro,
mentre per le righe in cui non si ha un match tale proprietà verrà messa NULL.
29 SELECT Student.NAME,StudentCourse.COURSE_ID
30 FROM Student
31 FULL JOIN StudentCourse
32 ON StudentCourse.ROLL_NO = Student.ROLL_NO;
31
2.3.2 Relazioni tra tabelle: Foreign Key
Una foreign key è una colonna che fa riferimento a un record di un’altra tabelle tramite la primary
key di questo. Viene quindi utilizzata per mostrare le relazioni tra le tabelle e fa da ponte tra
queste. La tabella in cui una foreign key viene definita è l’owning side della relazione, mentre
la tabella che viene referenziata dalla foreign key è l’ opposite side. Una foreign key può essere
definita nei seguenti modi:
33
Proprietà
1. Una record non può essere aggiornato se esistono delle FK che lo puntano (almeno per come
abbiamo dichiarato le chiavi sopra, vedremo tra un attimo come rimediare)
2. una FK deve referenziare una la PK della tabella a cui punta
La proprietà 1 definisce un vincolo molto forte nella relazione. Nella maggiorparte dei ca-
si vorremmo evitare una tale restrizione, per farlo si utilizzano rispettivamente le keyword ON
DELETE e ON UPDATE subito dopo la dichiarazione fatta sopra, ad esempio:
46
55 );
Queste keyword specificano cosa fare quando il record referenziato viene rispettivamente cancellato
e modificacto. I vari comportamenti e le loro rispettive keyword sono i seguenti:
1. CASCADE: è la più utilizzata, in quanto specifica che le modifiche effettuate sulla PK puntata
si debbano propagare alla tabella contenente la FK.
32
2.3.3 Tipi di relazione
Ci sono tre tipi di relazioni principali:
1. One-to-one: si ha quando ogni riga nella tabella1 ha solamente una riga a lei legata nella
tabella2. Ad esempio ogni azienda ha un solo CEO, un impiegato ha un solo ufficio etc.
2. One-to-many: è il tipo di relazione più comune. Si ha quando un record nella tabella1 è
legato a più record della tabella2 MA un record nella tabella2 è legato al più ad un record
della tabella1. Ad esempio: un libro contiene più capitoli ma un capitolo fa parte di un solo
libro.
3. Many-to-many: si ha quando un record nella tabella1 è legato a più record della tabella2 e
viceversa. Ad esempio: un supermarket vende prodotti prodotti di più marche e una stessa
marca può essere venduta da più supermercati diversi.
33
Capitolo 3
Spring
Spring Framework (o più brevemente Spring) è un framework java specificamente pensato per ri-
durre il carico di lavoro nello svilupo di una web application. Spring rimuove alcune delle maggiori
complessità legate all’utilizzo diretto degli EJB (Enterprise Java Beans) tramite l’utilizzo di un
project builder, come Maven o Gradle, e di tecniche quali AOP (Aspect-Oriented Programming),
l’utilizzo dei POJO (Plain Old Java Object) e l’implementazione del IoC (Inversion of Control ),
permettendoci di concentrarci sul codice e gestendo per noi l’infrastruttura dell’applicativo. Il fo-
cus principale di spring è quello di aiutare il programmatore nella gestione dei vari business objects,
permettendo lo sviluppo di applicazioni semplici, affidabili e scalabili con molta più facilità rispetto
ai framework e API classici, come JDBC, le JSP o le Servlet.
Spring si suddivide in più sotto-framework, detti moduli o layer, che possono essere usati sepa-
ratamente o in concomitanza e provvedono varie funzionalità alla nostra web-app.
Implementando l’IoC, cediamo il controllo al framework, lasciando che sia esso stesso a creare
le istanze e a ’iniettarle’ quando ve n’è bisogno. Per capire bene come funziona questo processo
dobbiamo introdurre alcuni concetti fondametali.
34
3.1.1 IoC Container
Un IoC container rappresenta il luogo in cui il framework salva le istanze dei vari oggetti che andrà
poi a iniettare. Il vantaggio di un tale contenitore è che è possibile avere una sola istanza per
ogni oggetto, riutilizzando sempre quella quando ve ne sia bisogno anzichè crearne ogni volta
una nuova.
Spring codifica questo container tramite l’interfaccia ApplicationContext, la quale è reponsabile di
istanziare, configurare e assemblare gli oggetti che andremo poi a iniettare, i quali vengono detti
Spring Beans (o anche solo beans quando chiaro dal contesto). Essa si occupa inoltre di gestire il
ciclo di vita di tali beans.
Spring fornisce inoltre varie classi che implementano questa interfaccia, come ad esempio Clas-
sPathXmlApplicationContext e AnnotationConfigApplicationContext. Ma come fa il container a
sapere quali classi vogliamo che siano gestite come bean? Tramite dei metadati di configurazione,
che possono essere dei file di configurazione in formato xml o delle annotazioni, i quali devono essere
passati ’in qualche modo’ al costruttore dell’ApplicationContext. In queste dispense utilizzeremo
esclusivamente le annotazioni, in quanto più facili sia da capire che da realizzare.
La corretta configurazione dell’IoC container è fondamentale per un corettamento funzionamento
della nostra app, tuttavia ciò non è sempre facile da fare. Vedremo in seguito che per fortuna
il team di Spring ci risparmia questo compito tramite l’utilizzo di Spring Boot, un meraviglioso
strumentopolo che approfondiremo tra un po’.
@Configuration
Se vogliamo utilizzare il primo metodo dobbiamo innanzitutto creare una classe di configurazione,
annotandola con @Configuration
@Configuration
public class MyConf {
.
.
.
}
All’interno di una tale classe dobbiamo definire dei metodi che restituiscano i vari bean desiderati,
ciascuno dei quali va annotato con @Bean. Supponiamo ad esempio di aver creato una classe
Negozio, per averla a disposizione come bean nel container dobbiamo fare cosı̀:
@Configuration
public class MyConf {
@Bean
public Negozio negozio() {
return new Negozio();
}
35
È importante capire che possiamo avere più classi di configurazione! Nonostante teoricamente tutti
i bean possano essere dichiarati all’interno di una stessa classe, è buona norma avere una classe
di configurazione per ogni macro-categoria di bean, in modo tale da garantire maggior chiarezza
della struttura del progetto. L’annotazione @Configuration deve inoltre andare a annotare la classe
principale del nostro progetto, che di conseguenza può essere utilizzata anch’essa per definire dei
bean.
@Component
Il secondo modo consiste nell’annotare direttamente la classe che si vuole trattare come bean con
l’annotazione @Component. In questo caso la classe verrà automaticamente riconosciuta come
candidata per essere un bean a compiletime e in seguito istanziata a runtime. Non abbiamo quindi
bisogno in questo caso di un file di configurazione aggiuntiva.
@Component
public class Negozio {
String address;
}
@Component
public class CentroCommerciale {
final String INDIRIZZO = "Via di Qui 69";
}
@Autowired
public setCentroCommerciale( CentroCommerciale centroCommerciale ) {
this.centroCommerciale = centroCommerciale;
}
@Autowired
private CentroCommerciale centroCommerciale;
36
• Core Container: consiste nei moduli Core, Beans, Context e Expression Language. Core
e Beans provvedono le parti fondamentali del framework, come l’IoC e la DI. Implementano
in maniera sofisticata il factory pattern, rimuovendo la necessita di programmare i singleton
e separando la configurazione e la specifica delle dipendenze dalla logica del programma.
Context si basa su Core e Beans e consiste in una via d’accesso agli oggetti framework-based.
Supporta l’internalizzazione, la propagazione degli eventi e il caricamento delle risorse. È la
sede dell’ApplicationContext.
Expression Language fornisce un potente linguaggio per la manipolazione e la query di un
grafo degli oggetti a runtime, consentendo ad esempio di recuperare gli oggetti tramite il loro
nome dall’IoC container.
• Data Access/Integration: consiste dei moduli JDBC, ORM, OXM, JMS e Transaction.
JDBC provvede un layer di astrazione che rimuove la necessità di programmare e gestire le
connessioni col database.
ORM provvede dei layer d’integrazione per le varie API relazionali, come JPA, JDO e Hiber-
nate. Tramite il suo utilizzo queste possono essere utilizzate in combo con le potenti feature
offerte da spring, ad esempio tramite la gestione dichiarativa delle transazioni col db (i.e. i
’motodi query’ che avete visto nel DAO).
OXM provvede un layer di astrazione per le implementazioni dei mapping Object/XML.
JMS consente di creare e inviare messaggi.
Transactions supporta la gestione dichiarativa e programmatica delle transazioni per tutti i
POJO.
• Web: consiste nei moduli Web, Web-Servlet, WebSocket e Web-Portlet.
Spring Web provvede delle feature di base per lo sviluppo web, come l’upload di file e l’i-
nizializzazione dell’ IoC container utilizzando dei servlet listener e un’application context
web-oriented.
Web-Servlet contiene l’implementazione del pattern MVC da parte di Spring.
Web-Portlet contiene una seconda implementazione del pattern MVC, da utilizzare nel caso
l’ambiente sia basato su Portlet anzichè Servlet. È identico al precedente per funzionalità.
• AOP: provvede un’implementazione del pattern AOP coerente con lo standard AOP-Alliance.
Esso ci permette ad esempio di definire dei interceptors che ci permettono di separare le parti
di codice che implementano funzionalità che dovrebbero stare disgiunte.
Aspects provvede l’integrazione con AspectJ.
Instrumentation provvede supporto per le classi e delle implementazioni dei classloader
utilizzabili in certi application server.
37
• Test: Supporta il testing delle component tramite tool esterni come JUnit o TestNG. Provve-
de inoltre un caricamento consistente dell’ApplicationContext e dei mock-objects che possono
essere utilizzati per testare componenti dipendenti da altre in totale isolamento.
38
possibile. @Controller, @Service, @Repository e @Component fanno tutte parte della dependency
org.springframework.stereotype, mentre @RESTController fa parte di org.springframework.web.
Domanda: tornate al terzo sprint, dove è stata utilizzata l’annotazione @Component? Perchè?
@RestController
@RequestMapping(value = "/utente")
@CrossOrigin(origins = "http://localhost:4200")
public class UtenteController {
@RestController
@RequestMapping(value = "/utente")
@CrossOrigin(origins = "http://localhost:4200")
public class UtenteController {
39
@PostMapping(value = "/insert")
public UtenteDto inserici(@RequestBody RegisterDTO dto) {
//operazioni che utilizzano il dto passato nella request
}
@RestController
@RequestMapping(value = "/utente")
@CrossOrigin(origins = "http://localhost:4200")
public class UtenteController {
@GetMapping("/findById")
public UtenteDto findById(@RequestParam(value="id", defaultValue=
"5") Long nomeACaso) {
...
}
@GetMapping("/findByUsername")
public UtenteDto findById(@RequestParam String username) {
...
}
• @ResponseBody va ad annotare il return type di un metodo nel caso in cui non si utilizzi
@RestController. Codifica l’oggetto restituito opportunamente nel body della response. Se
applicato alla classe del controller viene applicato automaticamente a tutti i metodi al suo
interno. Ha quindi senso dire che @RestController è equivalente all’unione di @Controller e
@ResponseBody come annotazioni a livello di classe.
• @ResponseStatus consente di specificare uno status voluto per la response.
• @ExceptionHandler annota un metodo che si fa carico di una determinata eccezione all’in-
terno del controller. Prende come argomento la classe dell’eccezione specifica che si vuole
gestire e può essere usato in ocmbinazione con @ResponseStatus per una gestione effica-
ce e facile da interpretare delle eccezioni. Un metodo annotato in questo modo viene ri-
conosciuto dalla JVM come adatto ogniqualvolta un’eccezione di quel tipo viene lanciata
all’interno del controller (ripassate la gestione interna delle eccezioni nel capitolo 1). Ad
esempio, se nell’esempio precedente passiamo un argomento invalido al metodo inserisci, que-
sto lancerà una InvalidArgumentException; per gestire questa eccezione definiamo il metodo
handleInvalidArgument come segue:
@RestController
40
@RequestMapping(value = "/utente")
@CrossOrigin(origins = "http://localhost:4200")
public class UtenteController {
@PostMapping(value = "/insert")
public UtenteDto inserici(@RequestBody RegisterDTO dto) {
//operazioni che utilizzano il dto passato nella request
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
void handleIllegalArgument(IllegalArgumentException exception) {
// ...
}
@Entity
È l’annotazione che specifica che una determinata classe è da considerare come un’entità. Affinche
tale annotazione sia valida, la classe in questione deve contenere una primary key. Per dichiarare
che un campo di un’entità deve essere trattato come chiave primaria si utilizza l’annotazione @Id.
Le annotazioni maggiormente utilizzate all’interno di un’entità sono le seguenti:
• @GeneratedValue è un annotazione field level (i.e. annota un parametro della classe) che
indica che quel valore deve essere generato automaticamente. Possiamo specificare la stra-
tegia per generare tali parametri come argomento dell’annotazione tramite la sintassi strate-
gy=GenerationType.STRAT, dove STRAT può essere rimpiazzato con uno tra AUTO, TA-
BLE, SEQUENCE, o IDENTITY. (Domanda bonus: guardando solamente la sintassi di
sopra, che oggetto è GenerationType? Risposta)
• @Table è un annotazione class-level che viene utilizzata per specificare il nome della tabella
e dello schema che lo contiene tramite gli argomenti name e schema rispettivamente. Può
essere omessa e in questo caso il nome della table coinciderà col nome della classe entità.
• @Column è un annotazione field-level che può prendere come argomenti varie configurazioni.
Ad esempio:
//...
@Column(name="STUDENT_NAME", length=50, nullable=false, unique=false)
private String nome;
//...
In questo caso il parametro nome della classe corrisponderà a una colonna chiamata STU-
DENT NAME che ammette valori lungii 50 caratteri, non può essere null e può contenere
uno stesso valore su più righe.
41
– @OneToOne viene utilizzata per implementare una relazione one-to-one tra entità. Deve
essere utiliizata assieme all’annotazione @JoinColumn, nella quale specifichiamo il nome
della colonna che contiene la primary key nella tabella a cui la foreign key punta, tramite
la sintassi @JoinColumn(referencedColumnName = ”id”). Nel caso in cui la relazione
sia bidirezionale, l’annotazione @OneToOne va specificata anche nella tabella che non
contiene la foreign key. Vediamo un esempio per capire meglio.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "address_id", referencedColumnName = "id")
private Address address;
@Entity
@Table(name = "address")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
//...
@OneToOne(mappedBy = "address")
private User user;
Nell’esempio dichiariamo una foreign key all’interno della tabella users che punta alla
tabella address. In questo caso la tabella che contiene la FK è users, di conseguenza è
lı̀ che dobbiamo inserire l’annotazione @JoinColumn.
– @OneToMany e @ManyToOne: supponiamo di avere due entità, ad esempio Carrello e
Item, e di volerle legare tra loro con una relazione one to many (in uno stesso carrello ci
possono stare più articoli). Utilizzando le annotazioni @OneToMany e @ManyToOne
possiamo procedere cosı̀:
@Entity
@Table(name="CART")
public class Cart {
/ /...
@OneToMany(mappedBy="cart")
private Set<Item> items;
42
@Entity
@Table(name="ITEMS")
public class Item {
//...
@ManyToOne
@JoinColumn(name="cart_id", nullable=false)
private Cart cart;
public Item() {}
Anche in questo caso la relazione è bidirezionale. È best practice salvare la foreign key
nel lato many-to-one della relazione, per questo è l’entità Item ad essere ownling side.
Ciò va fatto per ridurre la posiibilità di ambiguità.
3.6.4 @Transactional
Questa annotazione indica che una classe o un’interfaccia è responsabile per la gestione delle
transazioni col database. Generalmente può essere messa sia a livello di service che di DAO.
• @Data genera getter e setter per tutti i field della classe, denominati in base al nome
dell’attributo in camelCase (e.g. numeroTelefono -¿ getNumeroTelefono).
• @AllArgsConstructor genera un costruttore con tutti i field della classe come argomento.
• @NoArgsConstructor genera un costruttore privo di argomenti.
3.6.6 @Mapper
Questa annotazione indica che un interfaccia espone i metodi che si vogliono implementare per la
mappatura Entity< − >DTO da parte di MapStruct.
3.7 Hibernate
Hibernate è un ORM per gestione di persistenze du un database relazionale. Si occupa quindi di
generare automaticamente le query esplicite in SQL in base alle operazioni effettuate direttamente
sulle entità, risparmiando quindi questo compito allo sviluppatore ma soprattutto garantendo la
portabilità dell’applicazione. Per fare ciò fa uso delle annotazioni presentate nella sezione 2.6.3.
All’avvio dell’applicativo è hibernate che si occupa di generare le tabelle corrispondenti alle entità
che abbiamo definito all’interno del progetto, utilizzando le annotazioni per configurare le relazioni
tra esse. Il linguaggio utilizzato da hibernate si chiama hql (hibernate query language). Questo è
simile a sql nella sintassi, ma a differenza di questo opera non su record nel database ma su entità
della JPA, garantendo quindi portabilità e scalabilità. Grazie a SpringBoot abbiamo a disposizione
hibernate già configurato e pronto all’uso, tuttavia è comunque possibile fornire configurazioni ag-
giuntive all’interno dell’ application.properties. Una delle feature più potenti che ci offre hibernate
è l’utilizzo dei cosiddetti query-methods all’interno delle repository, che vengono poi tradotte da
hibernate in query in linguaggio nativo. Per apprezzare a fondo questo aspetto provate a dare
un’occhiata ai DAO delle prime due settimane e mettetelo a confronto con i DAO del progetto
finale!
43
Vediamo alcuni esempi di query con Hibernate. Per lo scopo di questi esempi, supponiamo di avere
prima creato un’entità Corso che ha al suo interno la proprietà ’nome’.
• Query-Methods: quando una query è relativamente semplice, è possibile dichiararla tramite il
nome di un metodo nella repository. La sintassi standard segue quella dei metodi già presenti
nell’interfaccia CrudRepository, che andiamo ad estendere quando creiamo le nostre:
@Repository
@Transactional
public interface CorsoRepository extends crudRepository<Corso,Long> {
• Query HQL: quando una query è troppo complessa per essere espressa efficientementte dal
nome di un metodo è consigliabile esplicitarla in hql. Per fare ciò annotiamo un metodo
con @Query, la quale prenderà come argomento la query che vogliamo eseguire. Il seguente
esempio mostra anche come far riferimento a uno o più argomenti del metodo all’interno della
query:
@Repository
@Transactional
public interface CorsoRepository extends crudRepository<Corso,Long> {
• Native query ci sono rari casi in cui hql potrebbe non bastare, in tal caso è possibile dichiarare
esplicitamente una query in SQL utilizzando sempre l’annotazion e@Query con parametro
nativeQuery = true.
• Criteria Query: Lecriteria query differiscono nettamente dagli esempi visti sin’ora, ne faremo
una trattazione concisa, rimandando a questo link per i dettagli.
Tramite la Criteria API, hibernate ci consente di costruire delle query come dei veri e propri
oggetti Java, seguendo un approccio funzionale tramite l’applicazione di più metodi conca-
tenati per la costruzione dell’intera struttura.
Per inizializzare una query su una entità, generiamo un’istanza della classe CriteriaQuery,
ad esempio utilizzando il metodo createQuery dell’interfaccia CriteriaBuilder, il quale prende
come argomento la classe dell’entità su cui vogliamo fare la query.
In seguito, creiamo una Root tramite il metodo .from() di myQuery. A questo punto siamo
pronti per strutturare la query. Vediamo un esempio esplicativo: costruisco una query equi-
valente a ”select * from corso where nome = ’matematica’ or id ¿= 12”. Tutta la costruzione
44
avviene attraverso il CriteriaBuilder, che espone dei metodi che codificano tutte le maggiori
operazioni che possiamo fare sul db.
critQuery.select(root).
.where(
mybuilder.or( //prende i due predicati su cui fare un
or come argomento
myBuilder.equal(root.get("nome"),"matematica"), //
testa uguaglianza
myBuilder.gt(root.get("id"),12) //testa >=
);
45
Capitolo 4
Rest API
4.1 REST
REST sta per Representational State Transfer. È un’architettura che definisce uno schema per lo
sviluppo di servizi web; una API che fornisce l’accesso a tali servizi in maniera semplice e flessibile
si dice REST API. La tecnologia REST è in genere preferita al più robusto SOAP (Simple Object
Access Protocol ) in quanto pesa di meno sulla banda. Tutte le comunicazioni in un’architettura
REST fanno uso delle richieste HTTP. Possiamo riassumere il funzionamento dell’architettura
REST come segue: una richiesta http viene mandata dal client al server, il quale manda indietro
una response http, la quale comunica l’esito della comunicazione al client. Una tale response può
essere codificata in vari formati, quali HTML, XML o JSON, che è solitamente il più usato per la
sua capacità di codificare informazioni complesse in maniera facilmente leggibile sia da un umano
che da una macchina.
46
4.3 Standard d’Industria
Nello sviluppo di una REST API è importante seguire alcune regole e best practices che costituisco-
no di fatto lo standard industriale, permettendo una maggiore velocità nello sviluppo e garantiscono
che l’API sia facilmente comprensibile a un utente esterno. Le principali regole da seguire sono le
seguenti:
1. Interfaccia uniforme: è un vincolo chiave, che suggerisce la presenza di un modo uniforme
per interagire con un dato server, indipendentemente dal dispositivo utilizzato o dal tipo di
applicazione. Le quattro linee guida per seguire questa regola sono:
• Resource Based:
• Manipolazione delle risorse tramire rappresentazioni: il client deve essere in possesso
di una rappresentazione di una risorsa che contenga abbastanza informazioni per per-
mettergli manipolarla (qualora in possesso dei dovuti permessi) tramite le chiamate
REST.
• Self-descriptive Messages: ciascun messaggio deve includere abbastanza informazioni su
come deve essere processato, in modo tale che il server possa analizzare facilmente la
request.
• Hypermedia as the Engine of Application State: ogni response deve contenere dei
collegamenti che consentano al client di trovare altre risorse in maniera semplice.
2. Stateless: ogni request deve essere indipendente dalle altre, ovvero deve contenere in sè lo
stato necessario per la sua gestione; inoltre il server non deve salvare nulla riguardante la
sessione. In un architettura REST il client deve includere tutte le informazioni di cui il server
ha bisogno per gestire una request all’interno della request stessa, ad esempio come parametri
dell’header.
3. Cacheable: ogni response deve includere se può essere o meno conservata nella cache e se
sı̀ per quanto tempo. Una gestione accurata della cache elimina totalmente o parzialmente
alcune delle interazioni client-server, andando a ridurre notevolmente il peso sulla largheza
di banda.
4. Client-Server: Le applicazioni REST dovrebbero seguire un’architettura Client-Server, dove
il client richiede delle risorse e il server è colui che gliele fornisce. Client e server devono
essere oggetti separati, legati tra loro solamente dalle comunicazioni e non dovrebbero aver
bisogno di conoscere alcunchè sulla struttura dell’altro.
5. Layered System: l’architettura di un’applicazione dovrebbe essere composta da più layer.
Ogni layer deve essere indipendente dagli altri e non dovrebbe aver bisogno di alcuna infor-
mazione sulla loro business logic per funzionare correttamente.
47
Capitolo 5
Microservizi
I microservizi sono un approccio per sviluppare e organizzare l’architettura dei software secondo
cui quest’ultimi sono composti di servizi indipendenti di piccole dimensioni che comunicano tra
loro tramite API ben definite. Questi servizi sono controllati da piccoli team autonomi.
Le architetture dei microservizi permettono di scalare e sviluppare le applicazioni in modo più
rapido e semplice, permettendo di promuovere l’innovazione e accelerare il time-to-market di nuove
funzionalità.
Specializzazione
Ciascun servizio è progettato per una serie di capacità e si concentra sulla risoluzione di un problema
specifico. Se, nel tempo, gli sviluppatori aggiungono del codice aggiuntivo a un servizio rendendolo
più complesso, il servizio può essere scomposto in servizi più piccoli.
48
che possano lavorare in modo più indipendente e rapido. Ciò riduce i tempi del ciclo di sviluppo.
Potrai trarre un enorme vantaggio dal throughput aggregato dell’organizzazione.
Scalabilità e flessibilità
I microservizi ti consentono di scalare ciascun servizio in modo indipendente per rispondere alla
richiesta delle funzionalità che la tua applicazione supporta. Ciò permette ai team di ridimensionare
in modo corretto l’infrastruttura in base alle necessità, misurare in modo accurato i costi di una
funzionalità e proteggere la disponibilità dell’applicazione nel caso in cui il servizio sperimenti un
amento nella richiesta.
Semplicità di distribuzione
I microservizi supportano l’integrazione continua e la distribuzione continua, cosı̀ da poter pro-
vare nuove idee in modo più semplice e ripristinare impostazioni precedenti quando qualcosa non
funziona. Gli errori, dunque, influiscono di meno sul costo delle operazioni, permettendoti di spe-
rimentare, aggiornare il codice in modo più semplice e accelerare il time-to-market delle nuove
funzionalità.
Libertà tecnologica
Le architetture basate su microservizi non applicano un unico approccio all’intera applicazione.
I team hanno la libertà di scegliere gli strumenti migliori per risolvere i loro problemi specifici.
Di conseguenza, i team che costruiscono i microservizi possono scegliere il miglior strumento per
ciascun lavoro.
Codice riutilizzabile
Dividere il software in moduli piccoli e ben definiti permette ai team di utilizzare funzioni per più
scopi. Un servizio scritto per una certa funzione può essere utilizzato come blocco costruttivo per
un’altra funzionalità. Ciò permette all’applicazione di effettuare il bootstrap in modo indipendente,
poiché gli sviluppatori possono creare nuove capacità senza dover scrivere del codice da zero.
Resilienza
L’indipendenza dei servizi aumenta la resilienza di un’applicazione in caso di errori. In un’ar-
chitettura monolitica, un errore in un unico componente potrebbe avere ripercussioni sull’intera
applicazione. Con i microservizi, le applicazioni possono gestire completamente gli errori di un
servizio isolando la funzionalità senza bloccare l’intera applicazione.
19 sui servizi:
20 pom:
21 spring eureka client
22 application.properties:
23 eureka.client.enabled= true
24 eureka.client.register-with-eureka= true
25 eureka.client.fetch-registry= true
26 eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
27 classe main:
28 @EnableDiscoveryClient
29
49
37 client:
38 register-with-eureka: false
39 fetch-registry: false
40 instance:
41 hostname: localhost
42 logging:
43 level:
44 ’[com.netflix.eureka]’: ’OFF’
45 ’[com.netflix.discovery]’: ’OFF’
46 nel main:
47 @EnableEurekaServer
50
Capitolo 6
Il JSON Web Token è un open standard che definisce un modo compatto e e autonomo per tra-
smettere delle informazioni in maniera sicura sotto forma di un oggetto JSON. Un’informazione
trasmessa in questo modo può essere verificata grazie a una firma digitale. JWT supporta sia firme
basate su crittografia a chiave segreta (utilizzando l’algoritmo HMAC), che firme su crittografia a
chiave pubblica (ad esempio utilizzando l’algoritmo RSA o ECDSA).
Alcuni contesti in cui l’utilizzo dei JWT è utile sono:
• Autorizzazione: è la principale sfera di utilizzo per il JWT. Dopo aver effettuato il login,
l’utente riceve un jwt dal server e ogni request che invia deve includere quest’ultimo per
garantirgli l’accesso ai servizi garantiti dalle informazioni (claim) presenti nel token.
• Scambio di informazioni: i JWT rappresentano un buon modo per trasmettere informazioni
in maniera sicuro, in quanto grazie all’utilizzo di una firma digitale si può garantire la prove-
nienza di un tale token. In aggiunta, poichè la firma digitale viene generata tramite l’header
e il payload del token, questa garantisce anche l’integrità del messaggio.
6.1 Struttura
Nella sua forma compatta, un JWT consiste in tre parti separati da punti. Queste parti sono, in
ordine:
1. Header: consiste di due parti. La prima parteindica il tipo di token (ovvero dice che questo
oggetto è un JWT) mentre la seconda indica l’algoritmo utilizzato per la firma. Ad esempio
un header di un token che utilizza l’algoritmo HS256 appare cosı̀:
{
"alg": "HS256",
"typ": "JWT"
}
2. Payload: è la seconda parte del token che contiene i claim, ovvero delle asserzioni riguardante
un’entità (tipicamente un utente) e altri dati aggiuntivi. Ci sono tre tipi di claim: claim
registrati, claim publici e claim privati.
• I claim registrati sono un insieme di claim predefiniti che, pur non essendo obbligatori,
sono raccomandati quando si genera un JWT. Questi offrono uno standard per dei claim
ricorrenti, come ad esempio iss (che sta per issuer, ovvero chi ha generato il token), exp
(expiration date, data di scadenza), sub (subject, ovvero il detentore del toke, colui a
cui i claim fanno riferimento) e altri.
• I claim pubblici possono essere definiti a piacimento dagli utilizzatori dei JWT. Per
evitare le collisioni con l’algoritmo di crittografia questi devono essere definiti nella
IANA JWT Registry oppure come un URI che contiene un namespace resistente alle
collisioni. Una collisione consiste in una coppia di stringhe che vengono mappate in una
stessa stringa dall’algoritmo, poichè lo spazio di partenza è tipicament di cardinalità
maggiore rispetto allo spazio di arrivo (in quanto le funzioni di hash per essere utili
51
devono mappare una stringa in una più piccola), queste sono inevitabili ma possono
essere rese ’abbastanza rare’ utilizzando una funzione resistente alle collisioni.
• Claim privati: sono claim personalizzati, creati per la condivisione di informazioni ad
hoc tra due parti.
Un esempio di payload è il seguente:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
3. Signature: consiste nella codifica dell’header, del payload e di un eventuale segreto (i.e. una
stringa segreta in possesso unicamente delle parti che utilizzano il token per comunicare).
Tale codifica deve essere fatta tramite l’algoritmo specificato nell’header, ad esempio, per
firmare un token tramire l’agoritmo HMAC SHA256 dobbiamo fare cosı̀:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
La firma viene utilizzata per verificare l’integrità del messaggio e che il mittente sia effetti-
vamente chi dice di essere.
6.2 Funzionamento
Dopo che l’utente ha effettuato il login, esso riceve un token che contiene delle informazioni sul-
l’utente e che gli fornisce delle credenziali per l’accesso ai servizi. A causa di questo, il token deve
essere trattato con grande cura per evitare problemi di sicurezza; in particolare, bisogna assicu-
rarsi che il token scada dopo un certo periodo di tempo relativamente breve, in modo da renderlo
inutilizzabile qualora intercettato. Sempre per ragioni di sicurezza, è consigliato non salvare infor-
mazioni sensibili nello storage del browser.
Ogniqualvolta un utente ha bisogno di accedere a una risorsa/path protetto, deve includere il JWT
nella request, tipicamente nel parametro ’Authorization’ dell’header, seguendo lo schema Bearer.
Questo schema di autenticazione è stateless e quindi in linea con un’architettura REST.
Quando una request arriva a un path protetto, questo controlla se un token è presente nell’header
e ne verifica la validità. Se tale verifica va a buon fine garantisce l’accesso alle risorse richieste,
altrimenti restituisce una response negativa.
Poichè i token vengono mandati nell’header della request, bisogna fare attenzione a cosa includere
nel payload in modo da non renderli troppo grandi, in quanto alcuni server hanno un limite di 8kb
per le dimensioni dell’header.
6.3 Implementazione
Per quanto riguarda il lato pratico, per implementare il JWT nella nostra app full stack dobbiamo
gestire la comunicazione sia lato front che lato back.
52
bisogno di due operazioni: una che intercetti una request e la cloni inserendo nell’header il token
e una che intercetti le response e salvi da esse il token aggiornato. Se autilizziamo il framework
Angular, possiamo implementare l’interfaccia HttpInterceptor, la quale espone il metodo intercept:
@Injectable()
export class ContraderInterceptor implements HttpInterceptor {
constructor(private jwtService: JwtService) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<
HttpEvent<any>> {
if (/*ho un token*/) {
const token = /*recupero il token*/
request = request.clone({ //inserisco il token
setHeaders:
{
Authorization: ‘Bearer ${token}‘
}});
}
return next.handle(request); //invio la request clonata
}
}
Per gestire anche le response modifichiamo il return utilizzando il metodo .pipe(), il quale in soldoni
crea l’equivalente di una stream con l’esito della request:
return next.handle(request).pipe(
tap(
event => {
if (event instanceof HttpResponse) {
//controlli vari ed eventuali sugli errori
const jwt = event.headers.get(’Authorization’);
//salvo il jwt opportunamente
}
}
)
)
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ContraderInterceptor,
multi: true
}
53
6.3.2 Back End
Lato back dobbiamo implementare un’infrastruttura che si occupi di verificare la validità di una
request prima di inoltrarla al resto del server. Vediamo come fare utilizzando il modulo Security
di Spring.
Includiamo inoltre nel pom la seguente dipendenza, che ci permette di operare con oggetti che
codificano un JWT in maniera semplice:
<dependency>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Iniziamo ora col creare una classe di configurazione, nella quale andremo a dichiarare i bean
necessari. Creiamo inoltre una component che ci andrà a fornire un encoder per le password:
@Configuration
public class MyWebConfiguration {
//qui inserirremo le configurazioni
}
@Component
public class MyEncoderUtil {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
NOTA: Nelle varie guide è spesso estesa la classe WebSecurityConfigurerAdapter, della quale
veniva overrode il metodo cofigure. Questa classe è stata recentemente deprecata, vediamo quindi
come fare senza di lei.
Creiamo ora un nuovo package, nel quale andremo a definire tutte le classi di utility per la festione
dei JWT, torneremo alla fine alla classe di configurazione per mettere assieme i pezzi. Per una
corretta configurazione del JWT abbiamo bisogno di:
• Generare il token
• Validare il token:
1. Controllare la firma
2. Controllare claim e permessi
TokenManager
Per prima cosa, creiamo la classe TokenManager, che sarà responsabile di creare e validare i token:
@Component
public class TokenManager implements Serializable {
//note
public String generateJwtToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
54
return Jwts.builder().setClaims(claims).setSubject(userDetails.
getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() +
TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, jwtSecret).compact();
}
//note
public Boolean validateJwtToken(String token, UserDetails userDetails)
{
String username = getUsernameFromToken(token);
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
Boolean isTokenExpired = claims.getExpiration().before(new Date());
JwtUserDetailsService
La seconda classe che creiamo è JwtUserDetailsService, che implementa la classe UserDetailsService
di Spring Security concretizzando il metodo loadUserByUsername():
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Override
55
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
)
}
JWT Controller
Creiamo ora un controller che sarà responsabile dell’autenticazione:
@RestController
@CrossOrigin
public class JwtController {
//bean creati da noi
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@PostMapping("/login")
public ResponseEntity<> createToken(@RequestBody LoginDto dto) throws
Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
dto.getUsername(),
dto.getPassword()
)
);
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
56
return ResponseEntity(headers, HttpStatus.OK); //qui possiamo
inserire anche un body prima dell’header, per questo esempo ci
interessa solo il token
}
}
JwtFilter
Prima di vedere come creare l’AuthenticationManager, occupiamoci del filter. Questa classe è
usata per intercettare ogni request in entrata e vedere se contengono un token valido nell’header.
Se cosı̀ non fosse restituiamo un errore 401 (Unauthorized).
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService userDetailsService;
@Autowired
private TokenManager tokenManager;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
SecurityContextHolder.getContext().setAuthentication(
authenticationToken);
}
}
filterChain.doFilter(request, response);
57
}
}
@Component
public class JwtAuthenticationEntryPoint implements
AuthenticationEntryPoint,
Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse
response,
AuthenticationException authException)
throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Unauthorized");
}
}
@Configuration
public class MyWebConfiguration {
@Autowired
private JwtAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtFilter filter;
@Autowired
MyEncoderUtil myEncoderUtil;
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http)
throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(myEncoderUtil.passwordEncoder())
.and()
.build();
}
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) {
http.csrf().disable()
.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(
authenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.
STATELESS);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.
class);
58
return http.build();
}
}
59
Parte II
Front-End
60
Capitolo 7
TypeScript
7.1 Javascript
Una delle cose più sorprendenti di JavaScript (che d’ora in poi indicheremo solamente con JS per
brevità) è che nonostante sia un linguaggio orientato a oggetti, in esso non è presente il concetto
di classe (almeno fino a ECMAScript 6). Consideriamo ad esempio il seguente programma:
eroe.nome = "ContraderMan";
eroe.forza = 100;
Quello che abbiamo appena fatto è stato inizializzare un oggetto (eroe) e definirne due proprietà:
nome e forza. In JS non abbiamo una dichiarazione tipizzata come in java (ovvero per dichiarare
un oggetto non dobbiamo specificare di che tipo questo oggetto sia), per dichiararre una variabile
utilizziamo solamente la keyword var. NOTA BENE: la tipizzazione è comunque presente ma è
JS stesso a capire con che tipo di dati ha a che fare e a prendere gli accorgimenti necessari. JS è
quindi un linguaggio weakly typed.
In un certo senso, è utile pensare a un oggetto di JS come a una mappa <String, Object> Java,
ad esempio quanto appena visto si potrebbe pensare come:
eroe.put("nome", "ContraderMan");
eroe.put("forza", 100);
7.1.1 JS Functions
Un concetto nuovo che JS introduce rispetto a Java è quello di Function. In JS una funzione
è semplicemente un valore di tipo Function. Riprendiamo l’esempio precedente e creiamo una
funzione che assegnamo a una variabile myFunction. Settiamo poi una nuova proprietà dell’oggeto
eroe utilizzando questa variabile.
61
var myFunction = function() {
console.log("La mia funzione");
}
eroe.parla = myFunction;
Possiamo chiamare delle function come se fossero dei metodi Java, ad esempio per chiamare la
funzione definita sopra scriveremo semplicemente eroe.parla().
La cosa che più si avvicina a questo approccio in Java è l’utilizzo di un’interfaccia funzionale. Ad
esempio, una possibile interpretazione di quanto appena fatto in Java potrebbe essere:
public parla() {
this.power.use();
}
eroe.setPower(
() -> S.out.println("Nella mia funzione");
)
eroe.parla();
}
Vediamo ora come cambia l’utilizzo della keyword this: saremo sorpresi di scoprire che JS ci
permette di fare delle cose abbastanza carine rispetto a Java.
Iniziamo con un esempio:
var superMan = {
heroName: "SuperMan",
sayHello: function() {
};
superMan.sayHello();
62
Iniziamo con l’osservare come abbiamo dichiarato direttamente le proprietà dell’oggetto su-
perMan: tramite coppie (nome,valore) separati da due punti. Questo tipo di notazione è detta
JavaScript Object Notation, per gli amici JSON. In secondo luogo notiamo come abbiamo referen-
ziato la variabile heroName all’interno dello stesso oggetto, in maniera analoga a quanto faremmo
in Java. Ma che succede se tramandiamo la funzione a un oggetto che non ha nessuna variabile
heroName al suo interno? Ad esempio:
nuovaFunzione();
In questo caso otterremmo in output ”Ciao, mi chiamo undefined”. Per ovviare a questa
problematica, in JS possiamo passare il contesto a cui deve fare riferimento la keyword this tramite
il metodo .call, ad esempio se avessimo scritto:
var spiderMan = {
heroName: "Spiderman"
};
nuovaFunzione.call(spiderman);
avremmo ottenuto in putput ”Ciao, mi chiamo Spiderman”, in quanto la keyword this va a cercare
all’inteno dell’oggetto spiderman la proprietà ad essa associata.
7.1.2 Ereditarietà in JS
In JS non abbiamo ereditarietà a livello di classi: gli oggetti possono estendere direttamente
altri oggetti. Più nel dettaglio, ogni volta che definiamo un oggetto questo avrà una proprietà
implicita che punta a un ’oggetto padre’. Tale proprietà si chiama proto e l’oggetto a cui punta
è detto il prototype. Per questo motivo, questo tipo di ereditarietà viene chiamata Prototypical
Inheritance. Vediamo come funziona: quando chiamiamo una proprietà di un oggetto, JS la
cercherà inizialmente nell’oggetto stesso e, se non la trova, proverà nel prototype, poi nel prototype
del prototype e cosı̀ via. Vediamo un esempio per capire meglio:
var supereroe = {
editor: "EditorSerioSRL"
};
spalMan.__proto__ = supereroe;
Il concetto di ereditarieta prototipica permette di svolgere in JS qualsiasi task che si può svolgere
in Java tramite l’ereditarietà classica. Ma che succede coi costruttori?
63
function SuperHero(nome, forza) {
this.nome = nome;
this.forza = forza;
}
Questa sintassi è tuttavia sconsigliata: supponiamo ad esempio di voler far si che tutti i supere-
roi abbiano un metodo sayHello() al loro interno. Ad esempio, lo potremmo inserire in un prototype
object degli oggetti SuperHero tramite la sintassi FunzioneCostruttore.prototype.nomeMetodo,
come di seguito
SuperHero.prototype.sayHello = function() {
console.log("Ciao, il mio nome e’ " + this.nome);
}
Tuttavia questa soluzione è artificiosa e di difficile lettura, inoltre non assomiglia per nulla a
Java! Per questo motivo si preferisce accantonare del tutto l’utilizzo delle funzioni costruttore in
favore di Object.create. Vediamo subito un esempio per capire di che si tratta:
var superHeroPrototype = {
sayHello: function() {
console.log("Ciao, il mio nome e’ " + this.nome);
}
};
Per maggiori dettagli su come utilizzare a pieno questo meccanismo rimandiamo a questo articolo.
64
}
superMan.fly(
() -> S.out.println("In volo verso " + destinazione)
);
}
In Java, ogniqualvolta referenziamo una variabile all’interno di una lambda, questa deve essere
(effectively) final, ovvero non deve cambiare. Se ad esempio nel main avessimo scritto
superMan.fly(
() -> S.out.println("In volo verso " + destinazione)
);
destinazione = "Terra";
function createHero(nome) {
return {
fly: function(destination) {
console.log(heroName + "in volo per" + destination);
}
In questo caso di fatto l’unico modo per accedere alla variabile heroName è attraverso la funione
fly, la quale di fatto protegge l’accesso a heroName alla stessa maniera di un getter.
Ad esempio
superman.fly("Marte");
console.log("Il nome da eroe e’ " + superman.heroName);
avremo in ordine ”Superman in volo verso Marte” e ”Il nome da eroe è undefined”. Una funzione
come createHero che incapsula delle informazioni accessibili solo tramite delle interfacce (che sono
restituite come Function nel return) è detta modulo.
65
7.1.5 Scope delle variabili
Un altra differenza da ricordare è l’assenza di uno scope per le variabili. Ciò si traduce, ad esempio,
nel fatto che una variabile utilizzata in un loop sia visibile anche ad di fuori di esso. Ad esempio:
function counterLoop() {
counterLoop();
È interessante anche notare come alla linea 3 non abbiamo un errore: questo in quanto l’interprete
scansiona l’intera funzione e salva la lista delle variabili al suo interno e solo dopo inizia a inter-
pretare la funzione riga per riga. Per via di questo comportamento, è best practice dichiarare tutte
le variabili che si andranno ad utilizzare all’inizio del codice, in modo da rendere il codice leggibile
e da evitare sorprese.
NOTA: a partire da ECMAScript 6 sono presenti anche le keyword let e const, entrambe le quali
ci consentono di dichiarare delle variabili con scope delimitato, con la seconda che consente anche
di rendere tali variabili immutabili. A differenza delle variabili dichiarate ocn var, quelle dichiarate
con let e const non sono disponibili fino a che la loro dichiarazione non è raggiunta nel codice, per
questo motivo dichiarare tutte le variabili all’inizio del codice è ancora più consigliato.
7.1.6 Classi in JS
Come anticipato all’inizio, a partire da ECMAScript6 è stato introdotto anche in JS il concetto di
classe. Queste sono di fatto delle ’funzioni speciali’ che presentano due componenti principali nella
sintassi: le dichiarazioni e le espressioni.
Per definire una classe tramite una dichiarazione utilizziamo la keyword class come segue:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
Per istanziare una classe si può utilizzare la keyword new, alla stessa maniera di Java.
Un altro modo per definire una classe è tramite un’espressione, ad esempio
66
let Rectangle = class { //o anche class Nome per assegnare un nome
diverso dalla variabile
constructor(base, altezza) {
this.base = base;
this.altezza = altezza;
}
};
console.log()
7.2 TypeScript
TypeScript (che per brevità abbrevieremo con TS) è un’estensione di JS che introduce tipi, classi,
interfacce e muùoduli opzionali. È importante ricordare che typescript usa lo stesso runtime di JS.
7.2.1 Tipi in TS
In Java, siamo abituati al fatto che ogni valore/oggetto sia di uno e un solo tipo. In TS ciò non
è più vero: i tipi sono un insieme di valori che condividono qualcosa tra loro. Pensare ai tipi
come degli insiemi ci permette di semplificare alcune operazioni, come ad esempio passare come
parametro di un metodo un valore che può essere o una stringa o un intero, in quanto non esiste
un tipo che rappresenta un tale valore.
In TS ciò diventa naturale appena realizziamo che pensare ai tipi come adegli insiemi ci permette
di fare tutte le operazioni insiemistiche con tali tipi. Nell esempio precedente potremo indicare
che il tipo del parametro deve essere l’unione del tipo/insieme stringa e del tipo/insieme number,
tramite l’operatore — :
Un’altra differenza con quanto siamo abituati a fare in Java è che gli oggetti in TS non sono
di un tipo preciso. Ad esempio, se costruiamo un oggetto che soddisfa una data interfaccia,
possiamo usare tale oggetto ogniqualvolta una tale interfaccia è richiesta, anche se non abbiamo
mai dichiarato esplicitamente una relazione tra l’oggetto e l’interfaccia. Ad esempio:
interface Pointlike {
x: number;
y: number;
}
interface Named {
name: string;
}
const obj = {
x: 0,
y: 0,
name: "Origin",
67
};
logPoint(obj);
logName(obj);
Poichè la const obj soddisfa sia l’interfaccia PointLike che quella Named, possiamo passare obj
come parametro sia di logPoint che di logName senza alcun problema.
I tipi in TS sono organizzati in maniera strutturale non nominale, ovvero sono le proprietà
al loro interno a sancire le relazioni tra tipi, non le loro dichiarazioni. obj implementa di fatto
entrambe le interfacce di sopra, nonostante nella sua dichiarazione ciò non figuri mai. (Se questo
tipo di libertà vi piace, clickate qui :) )
Alcune conseguenze della tipizzazione strutturale includono gli empty types e gli identical ty-
pes.
Vediamo di che si tratta esaminando due esempi:
class Empty {}
fn({ k: 10 });
In questo caso il codice è perdettamente valido in quanto l’oggetto k:10 ha tutte le proprietà di
Empty, in uqanto Empty non ha proprietà!
class Car {
drive() { //fa qualcosa }
}
class Golfer {
drive() { //fa qualcos altro }
}
//assegnamento valido
let w: Car = new Golfer();
Questo è un caso di identical types; anche in questo caso la dichiarazione è corretta in quanto
sia la classe Car che la classe Golfer hanno la stessa struttura. Per evitare errori, è buona norma
utilizzare identical types solo quando questi sono effettivamente correlati.
68
let obj: any = { x: 0 };
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
Vediamo ora come si applica la tipizzazione alle funzioni. In TS possiamo specificare sia il tipo
degli argomenti che il tipo dell’output di una funzione. La sintassi è la seguente:
stampaCoordinate({x: 3 , y: 4});
Per indicare che una proprietà di un oggetto è opzionale si fa seguire un punto di domanda al
nome della proprietà. Ad esempio, se vogliamo che la coordinata z sia opzionale nell’oggetto del
metodo precedente, scriveremmo:
stampaCoordinate({x: 3 , y: 4});
stampaCoordinate({x: 1, y: 2, z: 3});
Ogniqualvolta si opera con una proprietà opzionale bisogna sempre controllare che essa non sia
undefined, come abbiamo fatto sopra.
Approfondiamo un attimo sull’unione di tipi. OCme anticipato prima, in TS possiamo indicare
che un valore possa essere di un itpo oppure un altro utilizzando l’operatore di unione — . Quando
lavoriamo con tipi unione, dobbiamo stare attenti a utilizzare solamente le proprietà che sono
comuni ai due metodi. Se per esempio proviamo a scrivere
69
function stampaID(id: number | string) {
if ( typeof id === "string" ) {
console.log( id.toUpperCase() );
} else { console.log(id); }
}
Un concetto a prima vista inutile ma che nasconde un gran potenziale è quello di tipo letterale.
Un tipo letterale indica che una variabile può far riferimento solamente a un valore specifico, ad
esempio
L’esempio di sopra utilizzava delle stringhe, tuttavia i tipi letterali si possono utilizzare anche per
number e boolean. Il potenziale nascosto dei tipi letterali è che utilizzando unioni di questi si può
garantire che un qualcosa accetti solamente determinati valori. Ad esempio, possiamo garantire
che una funzione accetti solamente degli input predefiniti:
Vediamo infine due tipi primitivi che vengono utilizzati per indicare un valore assente o non ini-
zializzato: null e undefined. Il comportamento di entrambi questi tipi dipende dall’opzione di
compilazione strictNullChecks. Se tale opzione è disattivata, si può accedere a valoripotenzial-
mente null/undefined e questi possono essere assegnati normalmente. Se invece tale opzione è
attiva, bisognerà testare un valore potenzialmente null/undefined prima di poterci operare, come
avevamo visto in precedenza per le proprietà opzionali. Per il controllo su null possiamo testare
direttamente per uguaglianza con il tipo null, ad esempio
In alternativa, TS ci mette a disposizione l’operatore ! per indicare che un valore non deve essere
null/undefined. Tramite questo operatore, possiamo ad esempio scrivere:
70
Capitolo 8
HTML e CSS
In questo capitolo affronteremo le basi di html e css, due tool fondamentali per lo sviluppo di una
qualsiasi applicazione web in quanto responsabili della parte grafica e strutturale di questa.
8.1 HTML
HTML sta per HyperText Markup Language ed è il linguaggio di markup standard per la creazione
di pagine web. Esso si occupa did escrivere la struttura di una pagina tramite una serie di elementi,
i quali dicono al broswer come mostrare a schermo il contenuto.
8.1.1 Struttura
Ogni documento html ha una struttura ben precisa:
<!DOCTYPE html>
<html>
<head>
metadati e altre informazioni
<title>Page Title</title>
</head>
<body>
</body>
</html>
71
<tag> contenuto </tag>
ad esempio
Alcuni elementi, come l’elemento <br> di sopra (che serve per andare a capo), noon hanno nè
contenuto nè un tag di chiusura. Questi elementi sono detti empty elements.
Vediamo ora brevemente alcuni degli elementi più importanti:
• div: è un generico elemento html che viene solitamente utilizzato come contenitore per
dividere il contenuto del body in più zone. Il tag associato è <div>.
• Headings: sono definiti dai tag <h1>, <h2>, ... , <h6>, in ordine di importanza.
• Paragrafi: sono definiti dal tag <p>.
• Links: sono definiti dal tag <a>. Il tag di apertura contiene al suo interno l’attributo href,
nel quale si specifica la destinazione del link. Gli attributi vengono utilizzati in generale per
fornire informazioni aggiuntive su un elemento HTML, li vedremo più nel dettaglio a breve.
• Immagini sono definite dal tag <img>. Questo è un empty element. All’interno del tag
di apertura troviamo gli attributi src, alt, width e heigth, i quali indicano rispettivamente
la fonte dell’immagine, un testo da visualizzare qualora l’immagine non fosse disponibile, la
larghezza e l’altezza dell’immagine. Questi attributi non devono per forza andare nell’ordine
detto qui sopra.
L’attributo source può far riferimento sia a un’immagine hostata all’interno del sito sia a un
URL esterno.
8.1.3 Attributi
Vediamo più nel dettaglio alcuni attributi e le best practices legate al loro utilizzo.
Style
L’attributo style è utilizzato per specificare lo stile grafico di un dato elemento. È buona norma
utilizzarlo solamente in fase di test e specificare lo stile finale di una pagina all’interno di un foglio
di stile apposito, come vedremo nella seconda parte di questo capitolo. Ad esempio, per stilizzare
un paragrafo rendendo il testo rosso faremmo:
72
Lang
L’attributo lang viene utilizzato all’interno del tag <hmtl> per specificare la lingua della pagina.
Questo non influisce su come la pagina viene visualizzata ma aiuta i motori di ricerca a visualizzare
le pagine più rilevanti. Il linguaggio viene specificato attraverso delle abbreviazioni, come ”en” per
l’inglese o ”it” per l’italiano.
<html lang="it">
...
</html>
L’attributo title
Viene utilizzato per definire informazioni aggiuntive su un elemento, che verranno visualizzate
quando si passa sopra all’elemento col mouse:
Class
L’attributo class viene utilizzato per indicare un gruppo a cui l’elemento appartiene. È partico-
larmente utile quando si vuole far riferimento a più elementi in maniera concisa.
ID
L’attributo id viene utilizzato quando si vuole individuare univocamente un elemento all’interno
della pagina.
8.1.4 Tabelle
Le tabelle ci permettono di organizzare i dati all’interno di righe e colonne. Iniziamo con un
esempio e analizziamo i vari elementi che appaiono:
73
<table>
<tr>
<th>Company</th>
<th>Contact</th>
<th>Country</th>
</tr>
<tr>
<td>Alfreds Futterkiste</td>
<td>Maria Anders</td>
<td>Germany</td>
</tr>
<tr>
<td>Centro comercial Moctezuma</td>
<td>Francisco Chang</td>
<td>Mexico</td>
</tr>
</table>
• <table> è il tag che definisce la tabella, esso contiene al suo interno tutti gli altri elementi
che costituiscono i vari blocchi.
• <tr> (table row ) definisce una riga della tabella, contiene al suo interno tutti i blocchi relativi
a tale riga.
• <td> (table data) definisce un blocco base della tabella.
• <th> (table header ) definisce un blocco di intestazione della tabella.
La struttura di una tabella è abbastanza semplice, tuttavia questo è uno strumento molto potente
in quanto permette una facile organizzazione dei dati all’interno della pagina. HTML ci mette
inoltre a disposizione dei tag aggiuntivi che ci consentono di modellare più raffinatamente una
tabella. Alcuni di questi tag includono:
• <caption> consente di aggiungere un titolo alla tabella: <caption> Mio titolo </caption>.
• <colgroup> e <col> definiscono rispettivamente un gruppo di colonne e le proprietà
relative a quel gruppo di colonne.
• <thead> definisce l’header della tabella.
• <tbody> definisce il corpo della tabella.
• <tfoot> definisce il footer della tabella.
8.1.5 Liste
Le liste ci consentono di raggruppare elementi correlati tra loro sotto forma di liste. Le liste possono
essere principalmente di due tipi: unordered e ordered. In entrambi i casi, gli elementi della lista
sono specificati dal tag <li> (list item).
Unordered List
Una lista di questo tipo è specificata dal tag <ul> (unordered list), all’interno del quale sono
specificati gli elementi.
<ul>
<li>Caff</li>
<li>T</li>
<li>Latte</li>
</ul>
74
sarà visualizzata come segue
• Caffè
• Tè
• Latte
Orderd List
Una tale lista è specificata dal tag <ol> (orderd list). Gli elementi di questa lista sono associati a
un numero, in base all’ordine in cui vengono dichiarati. Ad esempio, la stessa lista di sopra sarà
visualizzata cosı̀:
1. Caffè
2. Tè
3. Latte
Description List
In aggiunta alle liste ordinate e non, HTML supporta le liste descrittive, definita dal tag <dl>
(description list). A differenza dei due casi precedenti, gli elementi di questo tipo di lista sono
composti da due tag: il primo è <dt> (data term) indica il nome dell’elemento, mentre il secondo
è <dd> (data description) si occupa di descriverlo. Ad esempio:
<dl>
<dt>Caff</dt>
<dd>100% Arabica</dd>
<dt>T</dt>
<dd>Qualita’ matcha verde</dd>
</dl>
<!--output-->
Caff
- 100% Arabica
T
- Qualita’ matcha verde
8.2 CSS
Il CSS (Cascading Style Sheet) è la porzione di codice che si occupa di stilizzare i contenuti di una
pagina web. Per applicare del CSS a una pagina html abbiamo due modi:
• Includere il codice nell’head della pagina all’interno di un tag <style>.
• Salvare il codice all’interno di un file apposito (con estensione .css) e poi importarlo nell’head
della pagina tramite un tag link:
Questo è l’approccio raccomandato, in quanto permette una più facile modularizzazione della
pagina. L’attributo rel sta per relationship e indica che il file che stiamo importando avrà il
ruolo di foglio di stile per la pagina.
Vediamo ora un esempio di codice CSS e analizziamone la struttura:
p {
color: red;
}
75
Questo snippet rende rosso il colore del testo in tutti gli elementi paragrafo della pagina. Esso
si divide in due macro parti: il selettore (in questo caso p) e il blocco delle dichiarazioni, ovvero
le regole della forma proprietà:valore che si vogliono applicare (in questo caso color: red;). Le
dichiarazioni devono essere racchiuse in un blocco tra parentesi graffe e separate tra loro da un
punto e virgola.
8.2.1 Selettori
Ci sono vari tipi di selettori, ad esempio, la p nel codice di sopra è detto selettore di elemento,
che seleziona tutti gli elementi di un tipo specifico, in questo caso tutti i paragrafi. Altri tipi di
selettore includono:
• Selettore di ID: seleziona l’elemento con l’id specificato. Ad esempio
html:
<p id = "my-unique-id"> Questo testo rosso </p>
css:
#my-unique-id {
color: red;
}
• Selettore di classe: seleziona tutti gli appartenenti a una data classe, ad esempio
html:
<p class="my-class"> Questo testo rosso </p>
<h1 class="my-class"> Anche questo testo rosso </h1>
css:
.my-class {
color: red;
}
• Selettore di attributo: seleziona gli elementi della pagina che hanno l’attributo specificato,
ad esempio:
html:
<p title="info"> Questo paragrafo lo modifico </p>
<p> Questo no</p>
css:
p[title] {
color: red;
}
html:
<p> Mio paragrafo </p>
css:
p:hover {
color:red;
}
76
8.2.2 Dichiarazioni
In CSS, le dichiarazioni ci permettono di stilizzare un qualsiasi elemento in maniera molto appro-
fondita. Vediamo un esempio per iniziare a prendere familiarità con alcune di queste:
body {
width: 600px;
margin: 0 auto;
background-color: #ff9500;
padding: 0 20px 20px 20px;
border: 5px solid black;
}
8.2.3 Position
Concentriamoci ora sulla proprietà position, che permette di specificare il tipo di posizionamento
di un elemento. Questa può assumere in tutto 5 valori:
1. static
2. relative
3. fixed
4. absolute
5. sticky
Inoltre, settare la proprietà position abilita l’uso delle proprietà top, bottom, left e right, che
consentono di posizionare effettivamente l’elemento. Il comportamento di queste ultime dipende
inoltre dal valore dato a position!
position: static
Questo è il tipo di posizionamento di default per gli elementi html. In questo caso le proprietà top,
bottom, left e right non hanno alcun effetto sull’elemento, il quale è sempre posizionato in accordo
con la struttura html della pagina.
77
position: relative
Questo tipo di posizionamento fa si che le proprietà top, bottom, left e right modifichino la posizione
dell’elemento rispetto alla sua posizione naturale. In questo caso la posizione degli altri contenuti
non verrà aggiustata per colmare eventuali spazi vuoti lasciati dall’elemento.
position: fixed
Questo tipo di posizionamento fa sı̀ che un elemento sia posizionato relativamente alla viewport,
che significa che l’elemento starà sempre allo stesso posto anche quando la pagina viene scrollata.
Ad esempio, se volessimo posizionare un elemento in basso a destra scriveremmo:
div.elemento {
position: fixed;
bottom: 0;
right: 0;
//altre eventuali dichiarazioni
}
position: absolute
Questo tipo di posizionamento fa sı̀ che un elemento sia posizionato relativamente all’elemento che
lo contiene che abbia anch’esso una dichiarazione position. Se un tale elemento non è presente
questo posizionamento fa automaticamente riferimento al body del documento. Gli elementi po-
sizionati in questo modo non seguono lo scorrere normale della pagina e possono sovrapporsi ad
altri elementi.
position: sticky
Un elemento con questo tipo di posizionamento è posizionato in base allo scroll in atto sull’elemento
che lo contiene. In pratica un elemento sticky cambia il suo posizionamento tra relative e fixed
in base alla posizione di scroll: è posizionato in maniera relative fino a che non si raggiunge una
posizione di offset data, raggiunta tale posizione diventa fixed. Ad esempio, stilizziamo una div in
modo che rimanga fissa in cima quando si raggiunge la sua posizione scrollando col mouse:
div.sticky {
position: -webkit-sticky; /* Safari */
position: sticky;
top: 0;
}
8.2.4 FlexBox
Il layout FlexBox (abbreviazione di Flexible Box ) provvede una maniera efficiente di disporre e
allineare elementi all’interno di un contenitore, anche quando le loro dimensioni sono sconosciute
e/o dinamiche. L’idea principale che sta dietro a quest’approccio è dare all’elemento contenitore
il potere di modificare ordine e dimensioni degli elementi al suo interno in maniera da gestire in
modo ottimale lo spazio disponibile in base al display del dispositivo utilizzato. Un flex container
si espande e riduce automaticamente a sua volta a tale scopo.
Iniziamo a vedere la terminologia e le proprietà associate a questo layout. Iniziamo con dare
un’occhiata alla seguente immagine per capire il flow degli elementi:
78
• main axis: è l’asse principale rispetto a cui gli elementi del container sono posizionati.
Questa non è necessariamente orizzontale, ma dipende dalla proprietà flex-direction (che
tratteremo tra poco).
• main-start — main-end: indicano le estremità rispetto a cui gli elementi sono posizionati.
• main size: corrisponde alla dimensione principale tra l’altezza e la larghezza.
• cross axis: è l’asse perpendicolare a quella principale. La sua direzione dipende dalla
direzione di quest’ultimo.
• cross-start — cross-end Indicano la direzione delle linee su cui vengono disposti gli oggetti.
• cross size è la dimensione secondaria rispetto alla main size.
79
6. space-between: gli elementi sono distribuiti uniformemente sulla riga, col primo attac-
cato all’inizio e l’ultimo attaccato alla fine.
7. space-around: gli elementi sono distribuiti uniformemente sulla riga.
8. space-evenly: gli elementi sono disposti in modo che lo spazio tra due elementi ’confi-
nanti’ sia sempre lo stesso.
• align-items: definisce come gli elementi sono disposti sulla loro riga rispetto al cross-axis.
I valori che può prendere sono:
1. stretch: (default) adatta le dimensioni degli elementi per coprire lo spazio disponibile.
2. flex-start / start / self-start: gli elementi sono disposti all’inizio del cross-axis. Le
differenze tra questi sono analoghe al caso del justify-content.
3. flex-end / end / self-end: come sopra ma rispetto alla fine del cross-axis.
4. center
5. baseline: gli elementi sono allineati in modo che le loro basi siano sullo stesso livello.
• align-content: è utilizzato per disporre le righe di un flex container quando vi è dello spazio
in più nella direzione del cross-axis. I valori che può prendere sono:
1. normal: (default)
2. flex-start / start: le righe sono organizzate all’inizio del container. Flex-start segue la
flex-direction mentre start la direzione di writing-mode.
3. flex-end/end: come sopra ma rispetto alla fine del container.
4. center
5. space-between: le righe sono distribuite uniformemente, la prima all’inizio del container
e l’ultima alla fine.
6. space-around / space-evenly / stretch: analoghe al caso di justify content ma per le
righe.
• flex-basis: definisce le dimensioni di partenza prima che lo spazio rimanente venga distri-
buito. Il valore può essere una lunghezza o una keyword:
1. auto: rimanda alla dimensione principale.
2. content: rimanda alla dimensione del contenuto dell’elemento.
• flex: combina flex-grow, flex-shrink e flex-basis, con il secondo e terzo parametro opzionale
e valori di default 0 1 auto. È raccomandata rispetto a usare le tre proprietà individuali per
brevità.
• align-self : sovrascrive il valore della proprietà align-items del container per l’elemento in
questione. Può prendere gli stessi valori di quest’ultima.
80
Capitolo 9
Angular
Angular è una piattaforma di sviluppo basata su TypeScript. Tra le sue features, angular include:
• Un framework componen-based per lo sviluppo di single page web-app scalabili.
• Un insieme di librerie built-in che provvedono una vasta varietà di funzionalità, tra cui
supporto alla navigazione, gestione dei form e comunicazione client-server.
• Una developer tools suite che copre le fasi di sviluppo, testing, build e update dell’applica-
zione.
Alcuni dei vantaggi e svantaggi comportati dall’utilizzo di angular includono:
Vantaggi
• Implementazione dell’architettura MVVM (Model View View-Model), che consente di isolare
la logica dal layer di interfaccia grafica e quindi di separare effettivamente le funzionalità
dell’app, rendendo più facile lo sviluppo e il mantenimento. In questa architettura, il layer
Model è rappresentato dai DTO che codificano gli oggetti con cui l’app avrà a che fare (che,
per garantire una corretta implementazioen di un’app full stack, devono essere uguali ai
DTO definiti nel lato server). Il layer View è invece rappresentato dai file .html e .css, che
curano appunto la parte grafica della nostra app. Il layer View-Model è infine dato dai file
TypeScript che regolano la dinamicità delle componenti, rappresentando di fatto la logica di
collegamento tra la View e il Model.
• Struttura dell’app in moduli, che consente di incapsulare varie funzionalità strettamente
legate tra loro all’interno di un unico scope.
• Utilizzo dei service e implementazione della Dependency Injection, con tutti i vantaggi che
questo comporta (vedere discussione qui Capitolo 3).
• Utilizzo di TypeScript come linguaggio, che consente di eliminare gran parte degli errori più
comuni in fase di sviluppo, rendendo il codice più pulito e facile da comprendere rispetto a
JavaScript.
Svantaggi
• Curva di apprendimento molto ripida, in particolare se paragonata ad altri framework quali
REACT o VUE.
• Verbosità e complessità.
• Limitatezza nelle opzioni delle SEO (Search engine optimization) e una poco supportata
accessibilità ai search engine crawlers (che di base consentono la calssificazione dei risultati
di una ricerca in ordine di rilevanza).
Per terminare l’introduzione, descriviamo brevemente come installare Angular e creare un primo
progetto vuoto. Per prima cosa, abbiamo bisogno di installare la command line interface (CLI)
di Angular. Per fare ciò possiamo affidarci a npm (Node Package Manager), che ci viene fornito
nell’installazione base di NodeJS (un potente runtime environment per JS).
Dopo aver installato Node apriamo un terminale e lanciamo il comando npm install -g @angular/cli
81
(il flag -g sta per global e rende anhular disponibile all’intero sistema). Terminata l’installazione,
possiamo creare la struttura base di un progetto direttamente sa linea di comando tramite ng new
nome-app. Questo comando genera in automatico tutte le configurazioni di base e inizializza il
modulo principale, inoltre ci consente qualora lo volessimo (ovvero sempre) di aggiungere le opzioni
di routing alla nostra app. Non male per un singolo comando!
Possiamo già lanciare la nostra applicazione base: spostiamoci all’interno della cartella del progetto
(che verrà creata automaticamente dal comando precedente) e lanciamo l’app sempre dalla cli
tramite ng serve -o (Dove il flag -o oppure l’equivalente –open aprono automaticamente il broswer al
localhost:4200, che è la porta di default per le app angular). Terminate le fasi iniziali, addentriamoci
nel funzionamento del framework!
9.1 Moduli
Un modulo, come anticipato in precedenza, consiste nella cellula base di un progetto Angular,
all’interno del quale vengono incluse tutte le funzionalità che sono legate strettamente tra loro.
Alla creazione del progetto, angular ci fornisce in partenza l’app-module. Questo è il modulo
principale dell’app, composto da 5 file:
• app.module.ts: è il file che gestisce di fatto l’applicazione: al suo interno vengono dichiarati
tutti gli altri moduli (sia interni che importati) che verranno utilizzati nell’applicazione.
Viene inoltre specificata la componente da bootstrappare all’avvio dell’applicazione.
• app.component.ts /.html /.css: sono i tre file che definiscono la componente principale
associata a questo modulo. Esploreremo più nel dettaglio le componenti nella prossima
sezione.
• app-routing.module.ts: è il file in cui vengono specificate le path, ovvero delle coppie url/-
component che verranno utilizzate dal router per la navigazione all’interno dell’applicazione.
In generale, per creare un nuovo modulo utilizzeremo il commando ng generate module nome-
modulo. Lanciando questo comando verrà in automatico creata una nuova directory, all’interno
della quale potremo un file nome.module.ts. Se vogliamo aggiungere un file per il routing inter-
no al modulo possiamo runnare lo stesso comando aggiungendo il flag –routing prima del nome.
Esaminiamo la struttura di un file *.module.ts: la prima cosa che notiamo è il decoratore @Ng-
Module. Questo definisce che un tale file contiene una classe che rappresenta un modulo angular.
Questo decoratore prende come argomento un oggetto JSON che viene utilizzato per specificare
varie funzionalità del modulo:
1. declarations: specifica le componenti contenute all’interno del modulo. Di default è vuoto
(in quanto non abbiamo ancora creato nessuna componente).
2. imports: specifica i moduli che vengono importati all’interno del modulo. Gli exportables (ov-
vero gli oggetti dichiarati con la keyword export presenti in questi moduli saranno utilizzati
all’interno di ogni template del nostro modulo.) Di default contiene CommonModule, il quale
fornisce le funzionalità di base di Angular che approfondiremo in seguito, e (se lo abbiamo
aggiunto) il relativo modulo di routing. Solamente queste prime due proprietà (declarations
e imports) saranno presenti alla creazione del modulo.
3. providers: nel quale vengono specificati tutti i moduli importati dei quali si vuole utilizzare
un injectable, ovvero un oggetto di cui si vuole iniettare la dipendenza all’interno di una
componente del nostro modulo.
4. bootstrap: la lista delle componenti che vengono automaticamente lanciate quando il nostro
modulo viene lanciato.
La classe decorata da @NgModule è generalmente vuota e dichiarata tramite la keyword export
se si vuole rendere disponibile il contenuto del modulo all’interno dell’app. Esaminiamo come
queste proprietà vengano utilizzate nel routing module: all’interno di questo file viene innanzitutto
dichiarata una const di tipo Routes (ovvero un array di oggetti Route), la quale specifica una lista
di coppie url/component La struttura di tale array è la seguente:
82
Un oggetto di tipo route comprende inoltre la proprietà opzionale children, la quale prende come
valore un array di Route, i path delle quali sono da considerarsi relativi al path della route madre.
Ad esempio:
Questa const verrà poi passata come argomento al metodo forRoot del RouterModule all’interno
degli import, il quale crea e configura un modulo con tutte le routes e le direttive fornite. Alla fine
la struttura sarà qualcosa di simile:
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class NomeRoutingModule {}
9.2 Componenti
Le compoonenti sono il building block principale di una applicazione angular. Ciascuna di esse
consiste di tre file:
1. un template HTML che specifica ciò che viene mostrato a schermo.
2. una classe TypeScript che regola il comportamento della componente.
83
All’interno della classe (dichiarata come exportable) troviamo invece tutta la logica che regola il
comportamento della view. Di default essa implementa l’interfaccia OnInit, la quale espone un
metodo (ngOnInit(): void ) che consente di specificare tutte le azioni da effettuare ogni volta che la
componente viene lanciata. Contiene inoltre un costruttore, il quale verrà utilizzato principalmente
per andare a iniettare le dipendenze che ci occorrono (vedremo più nel dettaglio come in seguito).
Ad esempio, la seguente componente contiene una variabile che viene inizializzata appena la classe
viene creata:
@Component({
selector: ’app-superdummy’,
templateUrl: ’./superdummy.component.html’,
styleUrls: [’./superdummy.component.css’]
})
export class superdummyComponent implements OnInit {
miaVar: string;
constructor() { }
ngOnInit(): void {
miaVar = "Sono inizializzata";
}
Il metodo ngOnInit è detto lifecycle hooks, ovvero un metodo che regola un momento della vita
della componente, in questo caso la creazione. Esso è l’unico di questi eventi che si deve obbliga-
toriamente implementare dal contratto con l’interfaccia, i restanti sono comunque utili e una loro
descrizione dettagliata è consultabile qui.
@Input()
Per utilizzare questo decoratore è necessario configurare sia la componente madre che quella figlia.
Supponiamo ad esempio di voler passare una stringa, innanzitutto dichiariamo la variabile nella
component figlia e decoriamola:
@Component({
selector: ’app-figlia’,
templateUrl: ’./figlia.component.html’,
styleUrls: [’./figlia.component.css’]
})
export class figliaComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
84
In seguito leghiamo tale stringa a un dato presente nella component madre come segue:
1. richiamiamo la componente figlia nel template della madre tramite il suo selettore CSS.
2. utilizzaimo il data binding per legare myVar alla variabile che vogliamo passare all’interno
del tag (ad esempio alla variabile ”varMadre” opportunamente dichiarata nella component
madre)
<app-figlia
[myVar]="varMadre"
></app-figlia>
@Output()
Questo decoratore funziona in maniera leggermente differente: esso decora una proprietà nella
component figlia che si vuole trasmettere alla component madre. Vediamo come:
@Component({
selector: ’app-figlia’,
templateUrl: ’./figlia.component.html’,
styleUrls: [’./figlia.component.css’]
})
export class figliaComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
2. Sempre all’interno della classe figlia si crea un metodo che emetta un evento con un valore
specifico. Tale valore è quello che ci interessa propagare alla component madre. Tale metodo
può essere triggerato ad esempio da un pulsante:
constructor() { }
ngOnInit(): void {
}
3. Nella componente madre, si crea un setter per la variabile che vogliamo ricevere dalla figlia:
85
varMadre: string;
4. Infine, si lega l’emitter della componente figlia al metodo creato in precedenza. Anche in
questo caso, questo viene fatto all’interno del tag CSS che specifica la componente figlia nel
template della madre.
<app-figlia
(myVar) = "setVarMadre($event)"
></app-figlia>
9.3.1 Interpolazione
L’interpolazione ci consente di visualizzare un dato definito nel TS della component all’interno
del template. Questo dato deve essere delimitato da una coppia di doppie parentesi graffe. Ad
esempio, se vogliamo mostrare a schermo il valore di una variabile scriveremmo
//nel TS
customer: CustomerDto; //al suo interno ho una property ’nome’
/* inizializzo il customer in qualche modo*/
//nell’HTML
<p> Nome del cliente: {{customer.nome}}</p>
L’interpolazione può essere usata anche all’interno dei tag, ad esempio per settare la fonte di
un’immagine:
//nel TS
myUrl: string = "sitosicuro.it/immaginesicuratrust";
//nell’HTML
<img src="{{myUrl}}" ... >
1. One-Way dalla source alla view (dal TS all’HTML): utilizza una coppia di parentesi quadre
e la sintassi [target]=”espressione” (anche l’interpolazione fa parte di questa categoria).
2. One-Way dalla view alla source: utilizza una coppia di parentesi tonde e la sintassi (target)
= ”statement”.
86
3. Two-Way che lega i dati dalla source alla view e viceversa. Utilizza una coppia di parentsi
”a banana” e la sintassi [(target)] = ”espressione”.
Il target di un data binding può essere una proprietà, un evento o anche il nome di un attributo.
Tramite data binding ci si può legare a un qualunque membro del TS che sia pubblico.
Esaminiamo ora i diversi tipi di binding, assieme ai target a cui fanno riferimento.
• Property Binding: può avere come target una proprietà di un elemento, di una component
o di una direttiva. Nel seguente esempio, facciamo property binding sulle proprietà alt, src e
ngClass:
• Event Binding: può avere come target un evento di un elemento, di una component o di
una direttiva. Nel seguente esempio, facciamo event binding su click e myClick:
• Two-way binding: può avere come target sia eventi che proprietà, ad esempio
9.4 Direttive
Le direttive Angular sono classi che provvedono dei comportamenti aggiuntivi agli elementi di una
applicazione. Queste si dividono in due tipi:
• Direttive d’attributo: modificano l’aspetto e il comportamento di un elemento, di una
componente o di un’altra direttiva.
• Direttive strutturali: cambiano il layout del DOM (document object model ), aggiungendo
o rimuovendo elementi del layout.
Direttive d’attributo
La più comuni direttive d’attributo built in sono NgClass, NgStyle e NgModel.
NgClass aggiunge e rimuove un insieme di classi CSS allo stesso tempo. Essa può essere usata con
espressioni ternarie o metodi appositi. Ad esempio:
87
currentStyles: Record<string, string> = {};
/* dichiaro tre booleani canSave, isUnchanged e isSpecial e li inizializzo
a true*/
setCurrentStyles() {
// CSS styles: set per current state of component properties
this.currentStyles = {
’font-style’: this.canSave ? ’italic’ : ’normal’,
’font-weight’: !this.isUnchanged ? ’bold’ : ’normal’,
’font-size’: this.isSpecial ? ’24px’ : ’12px’
};
}
Infine, si lega tale metodo alla proprietà ngStyle nel tag dell’elemento desiderato:
<div [ngStyle]="currentStyles">
Questa div ha di default font-style italic, font-weight bold e font-
size 24px.
</div>
NgModel viene utilizzato per mostrare una proprietà definita nella logica e per aggiornare tale
proprietà quando l’utente opera delle modifiche. È utilizzata in maniera particolare all’interno
dei form per legare un input a una variabile definita nel typescript corrispondente. Ad esempio,
supponiamo di aver definito una variabile nomeAttuale nel typescript e di volerla legare a un input
di un form:
In questo modo il contenuto di tale variabile sarà corrispondente al contenuto di tale elemento.
Direttive strutturali
Le direttive strutturali sono responsabili del layout HTML dell’applicazione. Queste possono ag-
giungere o rimuovere gli elementi a cui fanno riferimento. Le più comuni sono NgIf, NgFor e
NgSwitch.
NgIf aggiunge o rimuove un elemento in base al valore di un booleano: se tale valore è true l’ele-
mento (e tutto ciò che contiene) viene mostrato, se è false no. Per utilizzare questa direttiva basta
dichiararla nel tag dell’elemento tramite la sintassi *ngIf=”myBool”, dove myBool è il booleano
a cui si vuol fare riferimento. Questa direttiva supporta anche una forma if-else, con la seguente
sintassi:
ngFor viene utilizzato per mostrare una lista di elementi dinamicamente. Supponiamo ad esempio
di aver definito un array di stringhe items nel TS, per mostrarli tutti a schermo nel template si
utilizza *ngFor nel tag dell’elemento che si vuole ripetere. Ad esempio:
88
NgFor supporta anche la sintassi estesa *ngFor=”let item of items; let i=index”, la quale ci consente
anceh di avere a disposizione l’indice di ogni elemento che viene ripetuto. Ciò è particolarmente
utile quando si vuole far riferimento a un particolare elemento dell’iterazione.
NgSwitch sceglie che ’forma’ di un elemento mostrare in base al valore di una determinata variabile
interruttore. Esso imita la sintassi di uno switch Java/Javascript. Se ad esempio vogliamo cambiare
cosa viene mostrato all’interno di una div in base a una variabile orarioAttuale faremmo cosı̀:
<div [ngSwitch]="orarioAttuale">
<p *ngSwitchCase="mattina"> Buongiornissimo! </p>
<p *ngSwitchCase="pomeriggio"> Buon pomeriggio! </p>
<p *ngSwitchCase="sera"> Buonaseeeeeeeee’ </p>
//se orarioAttuale non ha nessuno dei tre valori precedenti mostro
questo
<p *ngSwitchDefault> WEWE AMICI DEL WEB</p>
</div>
Provider
Affinché una classe possa essere iniettata all’interno di un’altra, essa deve essere decorata con
@Injectable().
@Injectable()
export class MyService { ... }
In seguito, dobbiamo rendere disponibile tale dipendenza all’interno dell’app. Una dipendenza può
essere resa disponibile in tre livelli differenti:
• Component-Level : dichiarando la classe nell’array dei providers. Ad esempio:
@Component({
selector: ’my-comp’,
template: ’...’,
providers: [MyService]
})
In questo caso MyService (e tutte le sue funzionalità) sono disponibili all’interno di MyCom-
ponent e di tutte le componenti e direttive che vengono utilizzate nel template. Quando si
registra un provider a questo livello, si ottiene una nuova istanza ogni volta per ogni istanza
della componente.
• NgModule-Level : dichiarando la classe nell’array dei providers analogamente a sopra. In
questo caso, una stessa istanza della classe è disponibile per tutte le componenti del modulo.
• Root-Level : questo è fattibile in due modi. O dichiarando la classe nei providers del’app-
module oppure all’interno del decoratore @Injectable(), utilizzando la proprietà providedIn:
89
@Injectable({
providedIn: ’root’
})
export class MyService { ... }
In questo caso, una singola istanza del service è disponibile in tutta l’applicazione. Questo
consente inoltre ad angular di rimuovere il servizio dalla app compilata qualora questo non
fosse utilizzato, ottimizzand cosı̀ le prestazioni.
Consumers
Il modo più comune iniettare una dipendenza all’interno di una classe è utilzzare il costruttore.
Quando Angular crea una nuova istanza di una classe, determina di quali dipendenze questa abbia
bisogno guardando i tipi dei parametri dichiarati nel costruttore. Una volta che trova le dipendenze
necessarie (e eventualmente istanzia quelle mancanti) angular passa tali istanze al costruttore per
creare effettivamente la classe. Ad esempio, supponiamo di voler iniettare MyService all’interno di
una component:
@Component({
selector: ’my-comp’,
template: ’...’,
providers: [MyService]
})
export class MyComponent implements OnInit {
ngOnInit(): void {
}
9.6.1 Observable
Gli observable svolgono un ruolo di supporto nella comunicazione di più parti di un’applicazione.
Essi seguono il design pattern dell’observer in cui un oggetto detto subject gestisce una lista di suoi
dipendenti, chiamati observers e comunica ad essi ogni cambio di stato automaticamente. Gli Ob-
servable sono dichiarativi, ovvero essi non emettono i valori gestiti fino a che non vengono chiamati
da un utente, tramite una procedura detta subscribe. Vediamo quindi come vengono implementati
praticamente: per creare un observable in TS si crea un’istanza della classe Observable<T>, dove
T è il tipo degli oggetti che si vogliono gestire tramite quell’observable. Questa classe espone il
metodo .subscribe(), che viene utilizzato quando si vuole accedere agli oggetti gestiti e permette di
specificare come ottenere e generare valori o messaggi dall’observable. Il metodo subscribe pren-
de come argomento un observer : questo è un oggetto TS che definisce la gestione delle notifiche
inviate dall’observable. Il metodo subscribe restituisce un oggetto di tipo Subscription, il quale
contiene un metodo .unsubscribe(), che può essere utilizzato per terminare la ricezione di notifiche
dall’observable.
I tipi di notifica che un observable può inviare sono tre e e vengono gestiti da tre handler nell’ob-
server che si passa al metodo subscribe. Questi handler sono esplicitati come function e sono i
seguenti:
90
1. next: è l’unico handler obbligatorio, che definisce come gestire ciascun valore emesso.
2. error: è l’handler che specifica il comportamento da seguire in caso di errore. Un errore
termina l’esecuzione dell’observable.
const myObserver = {
next: (x: number) => console.log("Valore attuale: " + x),
error: (err: Error) => console.log("Errore inaspettato, termino l’
esecuzione"),
complete: () => console.log("Esecuzione terminata"),
};
// Logs:
// Valore attuale: 1
// Valore attuale: 2
// Valore attuale: 3
// Esecuzione terminata
91
di development (tipicamente la porta locale 8080 se lavoriamo con Spring) che in fase di produzione.
Questi URL ci serviranno quando andremo a fare le chiamate vere e proprie nei service. Vediamo
innanzitutto un esempio di un service base e analizziamolo passo passo. Per prima cosa, creiamo
una cartella service e apriamola nel terminale. Lanciamo quindi il comando ng generate service
nome-service, questo creerà automaticamente due file: nome-service.service.ts e un file con le
specifiche di testing, nome-service.service.spec.ts. Il service sarà già configurato come injectable
globale.
@Injectable({
providedIn: ’root’
})
export class DummyService {
constructor() { }
Per prima cosa importiamo la const evironment, che utilizzeremo per recuperare l’endpoint del-
l’API del back-end. In seguito, iniettiamo un HttpClient nel costruttore. Per fare ciò, dobbiamo
importare il modulo HttpClientModule nel modulo principale dell’applicazione e dichiararlo nei
provider sempre all’interno di questo. Questo oggetto espone al suo interno i vari metodi utilizzati
per effettuare le varie chiamate HTTP, semplificandoci notevolmente la vita.
Infine, definiamo i vari metodi che si occuperanno delle chiamate. Al loro internoutilizzeremo
i metodi forniti dal HttpClient, i quali restituiscono un Observable che gestisce dati del tipo
restituito dal metodo del backend che si invoca (qualora non si fosse sicuri si può anche passare any
come argomento generico dell’Observable). Vediamo come esempio cinque metodi che utilizzano
le cinque chiamate principali http, operando con un generico data transfer object.
92
import { DTO } from ’src/dtos/dto’; //questo e’ il file che contiene la
classe DTO al suo interno
@Injectable({
providedIn: ’root’
})
export class DummyService {
Poichè restituiamo un Obserable, quando chaimeremo questi metodi all’interno di una component
dovremmo utilizzare il metodo subscribe per poter accedere ai risultati. Questo è concatenabile in
maniera funzionale alla chiamata del metodo nel service. Vediamo un esempio, in cui utilizziamo
il metodo getAll per salvare tutte le entità di un database all’interno di un array.
@Component({
selector: ’app-superdummy’,
templateUrl: ’./superdummy.component.html’,
styleUrls: [’./superdummy.component.css’]
})
export class superdummyComponent implements OnInit {
entities: DTO[];
ngOnInit(): void {
this.service.getAll().subscribe({
next: (res: DTO[]) => this.entities = res,
complete: () => console.log(this.entities),
93
});
}
9.7.1 Sincronia
Come detto nella sezione precedente, l’utilizzo degli Observable offre notevoli vantaggi nella gestio-
ne degli eventi e nelle prestazioni, grazie all’utilizzo della programmazione asincrona, consentendo
al programma di iniziare task potenzialmente molto lunghe rimanendo comunque responsiva ad
altri eventi durante l’esecuzione, senza rallentare l’applicazione in attesa che questa finisca. Que-
sta tecnica di programmazione è in diretto contrasto con la programmazione sincrona, in cui il
programma aspetta che una task sia terminata prima di passare a quella successiva.
Nonostante i vantaggi che questo comporti in termini di prestazioni, alle volte vorremmo aspettare
che una task termini prima di passare a quella successiva; ad esempio, potremmo voler aspettare il
risultato di una chiamata http per evitare di operare con oggetti potenzialmente undefined. Per far
ciò, possiamo utilizzare il blocco next del metodo subscribe, nel quale tutte le operazioni vengono
eseguite in modo sincono. Vediamo però un metodo più generale per ovviare a questo problema,
attuabile ogniqualvolta si abbia a che fare con una funzione asincrona.
Una funzione asincrona è una qualunque funzione che opera in maniera asincrona, utilizzando una
Promise implicita per restituire il risultato. Essa viene indicato dalla keyword async. All’interno
di una tale funzione, possiamo utilizzare l’operatore await per comunicare al programma di atten-
dere l’esito di una Promise prima di passare alla riga di codice successiva. Vediamo un esempio:
creiamo artificialmente una funzione che restituisca un apromise al nostro scopo
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve(’Finito’);
}, 2000);
});
}
async myMethod() {
console.log("Effettuo la chiamata");
var x = await resolveAfter2Seconds();
console.log(x);
}
myMethod();
//log:
// Effettuo la chiamata
//Finito
Se non avessimo utilizzato la keyword async avremmo invece ottenuto come log:
Effettuo la chiamata
[object Promise]
in quanto il programma passa subito alla riga successiva, senza aspettare che la promise venga
effettivamente ’mantenuta’.
94
9.7.2 Caricamento dei dati
In questa sezione tratteremo di come angular carichi i moduli necessari al corretto funzionamento
dell’applicazione. Un modulo può essere caricato in due modi:
1. eager: in breve, tali moduli vengono caricati prima che l’application venga lanciata.
2. lazy: al contrario, tali moduli vengono caricati solamente quando c’è il bisogno.
Il modulo principale (AppModule) viene sempre caricato in maniera eager, in quanto responsabile
del funzionamento dell’intera app. I restanti moduli vengono invece caricati a discrezione del
programmatore. Per specificare il tipo di caricamento di un modulo, abbiamo i seguenti modi:
• Un modulo è caricato in maniera eager di default, di conseguenza si deve semplicemente
dichiarare negli import dell’AppModule, come discusso in precedenza.
• Per caricare un modulo in maniera lazy, questo non deve essere dichiarato negli import di
AppModule, bensı̀ dobbiamo operare nell’AppRoutingModule (il modulo di routing principa-
le) come segue: quando dichiariamo le routes, aggiungiamo la seguente route, dove al posto
della proprietà component specifichiamo la proprietà loadChildren:
95