Negli articoli di questa categoria proverò ad esporre tutti gli aspetti collegati alle applicazioni nello standard Java EE, introducendo il framework Struts (alla versione 1.3), Hibernate, JSF, JSTL, Spring, Jasper, ecc... Nonostante siano argomenti molto complessi e lunghi da esporre proverò introdurre tutti gli argomenti riportando anche degli esempi semplici per permettere a tutti di provare quanto esposto, non è detto che le nozioni qui descritte siano del tutto corrette e prendendo in mano un manuale, le definizioni o persino i nomi possono essere diversi perché non esiste una bibbia riconosciuta da tutti che regoli tutte le regole e le varie definizioni ma si possono trovare piccole differenze, basta osservare i più comuni siti web (tra cui wikipedia) per avere un idea della confusione delle definizioni, e di certo non sono così presuntuoso da pensare di fissare io le giuste definizioni e le corrette linee guida sulla programmazione ma voglio solamente esporre le cose nelle maniera più corretta possibile.

Per Java EE si indicano delle specifiche per la realizzazione di applicazioni web con il linguaggio di programmazione java e le tecnologie web HTML/CSS/Javascript, eventualmente con l'ausilio di un database o servizi esterni (webservice), software che rispettano queste specifiche vengono dette Applicazioni JAVA EE (Java Enterprise Edition) oppure J2EE (Java To Enterprise Edition), lo standard è così famoso e diffuso nel mondo informatico perché le applicazioni JavaEE rispettano una architettura multi-livello dove sono separate le componenti web (parte visuale) dalla parte logica di business e dalla parte delle informazioni (il collegamento con il database) quindi risultano molto facili da sviluppare e risultano molto stabili nel tempo nonostante la normale manutenzione, questo unito alla potenza del linguaggio Java rende lo standard JavaEE il migliore in assoluto per lo sviluppo delle applicazioni web.

La componente principale di una applicazione JavaEE sono le serlvet, che sono le classi Java che definiscono la separazione delle componenti che vanno a formare il modello Model-View-Controller, le servlet definisce la parte controller dell'applicazione mentre le tecnologie JSP e JSF, che permettono l'esposizione di pagine web, definiscono la parte view del modello, infine i JavaBean definiscono nel modello JavaEE la parte Model dell'architettura: classi Java che descrivono la forma dei dati in maniera consistente e completa, l'uso di queste classi permette nelle servlet e nelle pagine di usar l'oggetto completo senza dover manipolare le singole componenti.

Per partire con gli sviluppi di una applicazione JavaEE un programmatore deve almeno avere alcune basi: è fondamentale avere le basi della programmazione Java e in particolare la programmazione ad oggetti: bisogna conoscere bene i concetti di oggetto ed ereditarietà che verranno usati molto nei vari framework che vedremo, altre conoscenze importanti sono quelle legate alla programmazione web: HTML, Css e Javascript che compariranno molto negli articoli, cercherò di spiegare le funzioni javascript e le librerie Jquery usate negli articoli ma non mi metterò ad esporre tutti i linguaggi e le proprietà dei tag altrimenti non mi baserebbe il tempo e lo spazio web.

Per sviluppare applicazioni JavaEE bastano pochi programmi: un computer abbastanza recente da far girare la JVM senza problemi, un sistema operativo e la possibilità di scaricare ed eseguire programmi visto che avremmo bisogno di scaricare ed eseguire due componenti: Eclipse IDE for Java EE che può essere scaricato gratuitamente e servirà come ambiente di sviluppo e, inizialmente, servirà il server Tomcat, anche questo scaricabile gratuitamente, da notare che se usate un sistema MacOX o Linux vi consiglio di leggere gli articoli già presenti sul sito per scoprire come installare il server nel vostro sistema operativo, esistono varie versioni di Eclipse e Tomcat, vi consiglio di usare la più recente di Eclipse e la versione 7 di Tomcat per avere la maggior stabilità. Una volta scaricati i file zip basta copiarli in una cartelle e decomprimere i file per avere già un sistema pronto all'uso, per il momento non serve nessuna installazione e potete già lanciare l'eseguibile di Eclipse dalla cartella che avete decompresso dopo il download.

Ad ogni avvio del progrmama Eclipse. vi viene subito chiesto di indicare la cartella del workspace, cioè dovete selezionare una cartella dove il programma salverà i vostri progetti, è importante selezionare o eventualmente creare una cartella vuota così da non avere altri file che potrebbero disturbare il lavoro.

Eclipse è un programma composto da prospettive e relative viste, le prospettive si selezionano con i bottoni presenti in alto a destra nella videata, di default è presente la prospettiva "Java EE", per aggiungere altre prospettive bisogna andare nella voce "Prospective" nel menù "Windows"; ogni prospettiva ha la propria disposizione delle viste che possono essere spostate in tutte le posizioni della finestra, per spostarle basta cliccare e tenere cliccato sul titolo della vista per spostarle, per aggiungere una vista basta andare nella voce "show view" sempre nel menù "Windows"; ogni programmatore poi ama disporsi le viste a proprio piacimento.

Una volta aperto il programma dobbiamo prima di tutto configurare il server Tomcat, per fare questo basta andare nel menù "Windows" e selezionare la voce "Preferences", nella schermata delle preferenze basta trovare la voce "Servers" e entrare nella sottovoce "Runtime enviroment", qui bisogna cliccare sul tasto "Add" ed inserire i dati del nostro server: bisogna selezionare la versione corretta di Apache Tomcat e, dopo la pressione del tasto Next, basta selezionare la cartella dove abbiamo scaricato il server in precedenza, poi basta confermare le proprie scelte e chiudere le preferenze di Eclipse; poi bisogna andare nella vista Servers e cliccare sul link "No server are avaible" che compare nella vista, qui basta confermare la scelta che viene proposta selezionando il server appena collegato nelle preferenze. Confermando la scelta succederanno due cose: la prima è che nella vista Servers è comparso il server appena collegato e possiamo farlo partire e fermarlo con i comandi nella vista o premendo con il tasto destro del mouse, la seconda è la comparsa di un piccolo progettino nella vista "Project explorer" chiamato Server, in questo progetto ci sono alcuni file di configurazione del server Tomcat che possono essere modificati a seconda delle proprie esigenze (per il momento non modifichiamoli).

Per creare il primo progetto basta andare sul menù "File" e selezionare la voce "New", compare quindi l'elenco di tutti i tipi di progetti che possiamo creare e noi selezioniamo il tipo "Dynamic Web Project", nella finestra che compare dobbiamo dargli un nome (per esempio "Prova") e possiamo modificare le impostazioni di default del progetto infatti nella seconda videata conviene impostare alla voce Default output folder" il valore "WebContent/WEB-INF/classes", una volta confermato viene creato il progetto che compare nella vista "project explorer" e vengono creati i file fisici nel workspace. Prima di tutto bisogna aggiungere le librerie di Tomcat al progetto, andando sulle proprietà del progetto (con il tasto destro) bisogna cercare la voce "Build Path", selezionare la scheda "Libraries", e cliccando il bottone "Add Library", selezionare la voce "Server Runtime" e infine si seleziona la versione di Tomcat disponibile nel proprio Eclipse.

D'ora in avanti è possibile sviluppare nel progetto, quindi possiamo creare il file web.xml nella cartella "WebContent/WEB-INF" del progetto con dentro:

<web-app>
 <welcome-file-list>
  <welcome-file>index.jsp</welcome-file>
 </welcome-file-list>
 <display-name>MyWeb</display-name>
</web-app>

e poi creiamo la pagina index.jsp all'interno della cartella cartella "WebContent" con dentro il codice:

<%@ page language="java" import="java.util.*" pageEncoding="ISO-8859-1"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
 <head>
 <title>Prova</title>
 </head>
 <body>
 Pagina di prova
 <br/>
 BasePath = <%=basePath%>
 </body>
</html>

Poi basta aggiugere l'applicazione al server andando nella vista Servers e premere sul server Tomcat e selezionando la voce "Add and remove..." poi basta avviare il server con la voce "Start" sempre nella riga del server nella relativa vista. Il server Tomcat tipicamente viene avviato sulla porta TCP/IP 8080 ma altri programmi o altri server potrebbero usare tali porte, è possibile modificare il file server.xml nel progetto "Server" per modificare la porta del server nella riga:

<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>

mettendo una porta TCP/IP libera. Una volta avviato il server basta andare in un browser e inserire la riga:

localhost:8080/Prova/

per vedere la propria piccola applicazione avviata e rispondere correttamente con la jsp appena creata.

Non esiste un unico standard per la struttura di un progetto JEE, infatti si può costruire un progetto in vari modi, tutti validi, la struttura più famosa comunque prevede la separazione del codice java, le pagine jsp, tutti i componenti client come CSS e javascript e i componenti strutturali, in particolare su Eclipse è possibile strutturare un progetto:

  • /.settings/ : cartella di tutti i file di configurazione di eclipse, non vanno mai modificati a mano
  • /build/ : cartella destinazione di tutte le compilazioni tramite script ant (per ora non la useremo)
  • /src/ : cartella che contiene i sorgenti delle classi java e i file di properties
  • /WebContent/ : cartella radice del deploy, in fase di deploy sarà copiato tutto il contenuto di questa cartella
  • /WebContent/config/ : cartella che contiene i file di configurazione di struts1 e altri file
  • /WebContent/images/ : cartalla per le immagini (jpg, gif, png, ecc...)
  • /WebContent/META-INF/ : cartella con i metadati java (da non modificare a mano)
  • /WebContent/pages/ : cartella per le pagine jsp
  • /WebContent/scripts/ : cartella per i file javascript (file js) /WebContent/styles/ : cartella per i fogli di stile (file css)
  • /WebContent/WEB-INF/classes/ : cartella dove vengono compilate le classi java, la compilazione viene effettuata in automatico da eclipse
  • /WebContent/WEB-INF/lib/ : cartella per le librerie jar
  • /WebContent/WEB-INF/tags : cartella per i file tag
  • /WebContent/WEB-INF/tld : cartella per i file tld
  • /WebContent/WEB-INF/web.xml : file principale per la definizione dell'applicazione JEE
  • .project : file di configurazione di Eclipse (da non modificare a mano)
  • .classpath : file di configurazione di Eclipse (da non modificare a mano)

Da notare che la cartella settings e i file project e classpath iniziano per punto, ereditando da GNU Linux il concetto che i file che iniziano per punto son file nascosti, infatti questi file non devono essere modificati a mano ma dalle preferenze del progetto su Eclipse. Anche le classi java compilate (dentro alla WEB-INF/classes) non sono visibili dalla vista Java perché nascoste ma bisogna andare sulla vista Navigator per vedere con mano i file compilati.

Se si vuole esportare un progetto in formato War per poterlo installare su un altro web-server, basta cliccare sul progetto e selezionare la voce "Esporta" e poi selezionare il formato "War", da notare che il file generato può essere aperto con un comune programma di decompressione file (per esempio 7zip) e che all'interno sono presenti tutti i file e cartelle presenti nella cartella WebContent del progetto.

Nel primo progetto abbiamo scritto una pagina che viene caricata in automatico perché indicato nel file "guida" web.xml, in realtà questo sistema di navigazione tra componenti non viene mai usato e si preferisce sempre inserire dei componenti intermedi, questo per separare la parte back-end dalla parte front-end web, in questo articolo andiamo a costruire una servlet che, in parole semplici, è una classe java che risponde ad una "chiamata" del browser, in realtà è sbagliato pensare che le servlet servano solo per il protocollo web di tipo HTTP, infatti la classe GenericServlet non limita il suo uso al solo protocollo HTTP mentre la classe HttpServlet ovviamente è costruita proprio per il protocollo usato nel web; mentre si sviluppa una servlet bisogna sempre tenere a mente anche che, a differenza delle librerie per le applet AWT e Swing, le classi di tipo Servlet vengono eseguite nel server e quindi non possono avere componenti grafiche (GUI). Una classe servelt è composta da due proprietà una request (di tipo javax.servlet.http.HttpServletRequest) e una response (di tipo javax.servlet.http.HttpServletResponse), attraverso il primo oggetto è possibile accedere a tutte le informazioni utili come i parametri di input e gli oggetti in sessione mentre il secondo oggetto, inizialmente vuoto, deve essere valorizzato con la risposta che si vuole inviare al client e come detto prima è una risposta http se si tratta di pagine web, un altro oggetto a disposizione nelle classi servlet è la ServletContext (di tipo javax.servlet.ServletContext) con la quale è possibile accedere alle informazioni del contesto (context) di un’applicazione cioè tutte le informazioni generali. Per creare una Servlet, in termini pratici, significa definire una classe che estenda la classe HttpServlet con i metodi più comuni che eseguono l’overriding nella classe madre:

  • void doGet(HttpServletRequest req, HttpServletResponse resp) Gestisce le richieste HTTP di tipo GET. Viene invocato da service()
  • void doPost(HttpServletRequest req, HttpServletResponse resp) Gestisce le richieste HTTP di tipo POST. Viene invocato da service()
  • void service(HttpServletRequest req, HttpServletResponse resp) Viene invocato al termine del metodo
  • void doPut(HttpServletRequest req, HttpServletResponse resp) Viene invocato attraverso il metodo service() per consentire di gestire una richiesta HTTP di tipo PUT, per esempio nelle chiamate di caricamento file tramite protocollo FTP
  • void doDelete(HttpServletRequest req, HttpServletResponse resp) Viene invocato attraverso il metodo service() per consentire ad una Servlet di gestire una richiesta di tipo HTTP DELETE, per esempio per cancellare un file tramite protocollo FTP
  • void init() Viene invocato soltanto una volta dal Servlet Engine al termine del caricamento della servlet ed appena prima che la servlet stessa inizi ad esaudire le richieste che le pervengono
  • void destroy() È invocato direttamente dal Servlet Engine per scaricare una servlet dalla memoria
  • String getServletInfo() È utilizzato per ricavare una stringa contenente informazioni di utilità sulla Servlet (ad es.: nome della Servlet, autore, copyright). La versione di default restituisce una stringa vuota
  • ServletContext getServletContext() Viene usato per ottenere un riferimento all’oggetto di tipo ServletContext

Mettiamo in pratica queste piccole nozioni teoriche per creare la prima classe Servlet, il cui compito sarà quello di mostrare le tabelline dei numeri da 2 a 12 (così le ripassiamo):

package it.alnao.javaee.prova.servlet;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;

public class PrimaServlet extends HttpServlet{
 private static final long serialVersionUID = 1L;
 
 private static final String HTML_TOP = "<html><head><title>Primo esempio servlet: tabelline</title></head><body>";
 private static final String HTML_BOTTOM = "</body></html>";
 
 private static final String TABLE_TOP = "<h3>Tabelline</h3><table width='80%'>";
 private static final String TABLE_BOTTOM = "</table>";

 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
  response.setContentType("text/html");
  PrintWriter out = response.getWriter();
  out.println(HTML_TOP);
  out.println(TABLE_TOP);
  for (int i=2;i<=12;i++){
    out.println("<tr><td><b>"+i+"</b></td>");
    for (int j=2;j<=10;j++){
      out.println("<td>"+(i*j)+"</td>");
    }
    out.println("</tr>");
  } 
  out.println(TABLE_BOTTOM);
  out.println(HTML_BOTTOM);
 }


 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
   this.doGet(request,response); //notare che viene richiamato il goGet
 } 
  
 public String getServletInfo(){
   return super.getServletInfo();
 }
}

Abbiamo suddiviso il codice HTML generato dalla servlet in più parti: all'inizio vengono scritte le righe di intestazione dell'HTML (lette da una costante per comodità), poi viene fatto il ciclo dove vengono scritte le celle della tabella HTML delle tabelline e alla fine vengono chiusi i tag della tabella e della pagina. Nell'esempio di può notare come il codice sia (volutamente) mischiato, infatti c'è poco ordine tra codice HTML e codice Java, questo stile era adottato agli inizi della programmazione web prima dell'introduzione dei framework (come struts) e dei linguaggi xhtml e di JSTL che evitano quando possibile che un programmatore debba mischiare codice java (quindi server) con i vari linguaggi per la visualizzazione delle pagine (HTML, CSS, Javascript, ecc...). Nell'esempio, è stato fatto per comodità l'override anche del metodo doPost(), il cui codice si preoccupa semplicemente di inoltrare la richiesta al metodo doGet(). Per provare la servlet però bisogna prima censire la classe nel file web.xml dove andremo ad aggiunere

<servlet>
  <servlet-name>PrimaServletName</servlet-name>
  <servlet-class>it.alnao.javaee.prova.servlet.PrimaServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>PrimaServletName</servlet-name>
  <url-pattern>/PrimaServletUrl</url-pattern>
</servlet-mapping>

per poi provare da browser l'url http://localhost:8089/Prova/PrimaServletUrl

Infine c'è da notare che nella nostra classe servlet abbiamo importato alcune classi ma nessuna libreria jar, questo perchè abbiamo usato solo package java standard che sono già presenti nella JVM del web-server e caricati all'avvio dell'applicazione di prova.

Con file jsp si indica comunemente una pagina o una parte di pagina, questo file può contenere codice java che viene eseguito nel WebServer, più precisamente dalla JVM, e il codice HTML che viene inviato al client (che si solito è un browser), secondo le varie filosofie di programmazione l'uso di codice java "puro" all'interno delle pagine jsp viene ormai considerato deprecato e poco utile visto che si possono importare le varie librerie che mettono a disposizione alcune taglib evolute che danno la possibilità al programmatore di risparmiare codice ma nulla vieta di usare il codice java puro nei file jsp. Da notare che le pagine jsp non sono classi java e quindi non viene creato nel workspace di sviluppo un file class corrispondente perché il file jsp viene compilato runtime dal WebServer ogni volta che la pagina viene eseguita, in alcuni server evoluti si possono trovare i file class delle pagine jsp, file nascosti nelle cartelle temporanee che risultano incomprensibili anche se vengono decompilati.

La parte java della JSP viene compilata in maniera sequenziale dalla prima riga, quindi all'inizio di ogni file si possono inserire le direttive che servono al compilatore Java per compilare il resto della pagina, istruzioni simili alle import nelle classi Java, queste istruzioni vengno dette direttive e si indicano in una pagina con il tag:

<%@ include file="filedaincludere.jsp" %>

dove in questo esempio viene usato la direttiva include per includere nella corrente pagina un altro file jsp che verrà compilato assieme. Altre direttive molto usate sono sono:

<%@ page import="java.util.*" %>

per importare una o più classi e sono da usare come gli import java, usare una classe java in una jsp senza import ovviamente genera un errore di compilazione che però viene segnalato solo in fase di compilazione quindi solo quando la pagina jsp viene eseguita.

<%@ page contentType="text/html" %>

indica in quale formato sia scritto il documento (per esempio HTML o XML)

<%@ taglib prefix="myprefix" uri="taglib/miataglib.tld" %>

importa nella pagina JSP una tag-lib che essere identificata tramite una URI (questo argomento viene approfondito nei prossimi articoli).

Per quanto riguarda il codice java dentro le pagine jsp, di base si usano le scriptlet, sistema creato proprio per "mischiare" il codice HTML (client) e il codice Java (server) anche se questo sistema è considerato deprecato viene ancora usato ma molti anche per ragioni di retro-compatibilità nei progetti di grandi dimensioni. La prima disputa sul termine scriplet è se, in lingua italiana, il termine sia maschile (gli scriplet) o femminile (le scriptlet), ogni programmatore ha la propria opinione. In un corpo di pagina, una scriplet inizia con <% e termina con %>, per esempio:

<%
int i=0,
i++
%>

In particolare si possono usare scriplet anche per fare cicli o condizioni di elementi HTML, per esempio

<%
 for (int i = 0; i < 5; i++){
%> Ciao Mondo! <%
 }
%>

Si possono definire dei metodi proprietari della pagina con l'uso del <%!

<%!
public int void contaParole (String s){
 StringTokenizer st = new StringTokenizer(s);
 return st.countTokens();
}
%>

Ed è possibile definire il metodo jspInit che viene lanciato una sola volta e serve per inizializzare gli elementi in pagina e il metodo jspDestroy per la pulizia a fine caricamento.

Le scriplet vengono usate soprattutto per inviare al client dei valori dal server, cioè passare dei valori da Java all'HTML, l'uso del tag <%=, da notare che in questo caso è possibile inserire una sola riga e NON si deve mettere il ; (punto e virgola) finale (strano?!?!)

<%=i%>
<%=contaParole("Quante sono queste parole?")%>

Da notare che gli oggetti request, session, out, exception e application sono oggetti considerati impliciti nella pagina quindi non serve importarli e/o definirli e si possono usare senza problemi, per esempio è possibile usare

<% out.print("valore"); %>

al posto di

<%=valore%>

oppure si può usare

<%
String valoreId=request.getParameter("id");
session.setAttribute("id",valoreId);
%>

Ripeto ancora questi metodi di scrivere le pagine jsp è considerato deprecato e non dovrebbe essere usato, purtroppo troppo codice è stato scritto così e non si può vedere!

La caratteristica principale della programmazione di Java è l'uso delle classi e degli oggetti, ovviamente è possibile importare e usare oggetti nelle pagine JSP, se si vuole importare una classe in una pagina JSP basta inserire in testa al file il metatag

<%@ page import="it.alnao.manuali.java.NomeClasse"%>

oppure tutto un package

<%@ page import="it.alnao.manuali.java.*"%>

e poi usare la classe all'interno delle scriplet

<% NomeClasse.NomeMetodoStatico(); %>

Ma se si vuole usare un oggetto che è già istanziato in request o in sessione si può ridurre il codice scritto usando il tag usebean, per esempio:

<jsp:useBean id="NomeBean" scope="session" class="it.alnao.manuali.java.NomeClasse"/>

questo tag prende dalla sessione l'oggetto NomeVariabile, lo casta al tipo indicato e crea nella pagina una variabile di nome NomeVariabile con quel valore, cioè questo tag sostituisce l'istruzione

<% NomeClasse var=(NomeClasse) request.getSession().getAttribute("NomeBean"); %>

ed è molto usato in quei progetti dove vengono usate le scriplet e non linguaggi più evoluti come le jstl o i tag Struts, analogamente al tag useBean esistono i tag setProperty e getProperty che servono a valorizzare e leggere proprietà di un oggetto bean, la sintassi è

<jsp:setProperty name="NomeBean" property="NomeProp" param="Valore" /> 
<jsp:getProperty name="NomeBean" property="NomeProp">

Da notare che per usare questi tag non serve importare nessuna libreria jar perchè sono tag "standard" java e vengono riconosciuti automaticamente dal WebServer, è presente anche un tag

<jsp:import

simile al <%@ include file="filedaincludere.jsp" %> già visto in precedenza e in questa guida non lo introduco visto che è considerato deprecato quindi da non usare.

Una delle grandi potenzialità di Java è la presenza di moltissime librerie che possono essere installate nei progetti web in fase sviluppo in maniera veloce aggiungendo un file al proprio workspace, per libreria jar (Java Archive) infatti si intende un file con estensione jar che contiene una raccolta di classi java compilate (in formato class) che possono essere sparse in più package. L'uso di questi file è fondamentale quando si creano progetti web perché taglib, framework e librerie per il collegamento con i database saranno disponibili nel nostro progetto dopo aver configurato le librerie jar.

Per importare la libreria bisogna disporre del file jar, questi file di trovano sui siti dei progetti, per esempio i jar di struts sono disponibili nel sito ufficiale, trattando di progetto open-source troveremo tutto nei vari siti web anche se a volte raccolte di questi jar si trovano anche dentro lo stesso eclipse, i file jar sono quasi sempre compatibili con tutte le piattaforme e con tutti i webserver perché è proprio la JVM a garantire la compatibilità tra ambienti (altrimenti Java a che servirebbe?). Come già visto, nei progetti web è presente la cartella WEB-INF/lib/ dedicata a contenere tutti i file jar, basta infatti copiare dentro questa cartella il file jar con un semplice copia-incolla per avere il jar disponibile, se si esporta il WAR del progetto il file jar viene compreso automaticamente e sarà già disponibile a run-time, per quanto riguarda invece l’ambiente di sviluppo eclipse bisogna eseguire una configurazione sul classPath del progetto. Il classPath del progetto eclipse è un file all'interno del progetto (fuori da qualsiasi cartella) e si chiama con .classPath (con il punto iniziale per indicare che è un file nascosto almeno nei sistemi Linux), il contenuto di questo file indica al compilatore come e cosa compilare nel progetto, per esempio indica che la cartella sorgente è la “src” e la cartella di destinazione dei compilati è la WEB-INF/class e sono elencate tutte le librerie jar che il compilatore deve comprendere al momento della compilazione, per ogni jar c’è una riga e si può indicare un file all'interno del progetto o un file jar esterno, per esempio:

<classpathentry kind="lib" path="_libsWLS/struts.jar"/>
<classpathentry kind="lib" path="C:/cartella/file.jar"/>

ovviamente modificare questo file a mano non è la via corretta e quindi conviene usare gli strumenti di eclipse. Da notare che questo file NON viene compreso nelle esportazioni del WAR perchè si tratta di un file dedicato ad eclipse e non al funzionamento run-time nel webServer.

Per aggiungere un jar al classpath del progetto basta andare nella vista “Package explorer”, selezionare il file jar, tasto destro con il mouse e selezionare la voce “Build path” e “Add to build path”, per invece modificare o gestire l’elenco dei jar si deve andare nelle proprietà del progetto (tasto destro sul progetto e poi selezionare “Proprietà”) e alla voce “Java Build Path” e nella vista “Libraries” si può vedere l’elenco delle librerie con la possibilità di toglierne e inserirne di nuove, da notare che è presente anche il bottone “Add External Jar” per aggiungere librerie esterne al progetto.

E' importante notare che nel classPath (e nella vista delle proprietà) sono presenti anche le librerie java base “JRE System Library” ed eventualmente le librerie del webserver “Server RunTime”, queste si aggiungono in automatico oppure si possono aggiungere premendo il pulsante “Add library”.

Per progetti di grandi dimensioni o per motivi organizzativi, a volte bisogna eseguire l'esportazione di una libreria jar, per esempio in un gruppo di lavoro composto da più persone è possibile lavorare a componenti diverse senza problemi se una di queste è un jar, ovviamente non è possibile esportare tutto un progetto web in un jar (per le esportazioni di tutto il progetto web c'è il formato war), da notare che all'interno di una libreria java sarà possibile mettere anche file non java anche se questo è sconsigliato e conviene sempre creare librerie jar con solo classi java compilate e i file collegati. Per creare un proprio jar esistono varie procedure, più o meno automatiche, in questo articolo introduttivo verrà mostrato solo il metodo manuale di Eclipse ma nella sezione dedicata agli script ANT vedremo uno dei metodi automatici.

La procedura "manuale" per l'esportazione del jar è molto semplice: bisogna selezionare un progetto dentro al workspace di Eclipse, premere con tasto destro del mouse e selezionare la voce "Esporta", a questo punto basta selezionare la voce jar per far partire il tool di esportazione, nel primo step si selezionano i package (dalla cartella src) che poi andranno compilati ma sarà possibile aggiungere al jar anche altri file del progetto anche se questi non si trovano dentro alla cartella src, nella stessa videata si indica in quale cartella creare il jar e altre opzioni utili, nella seconda videata è possibile indicare se includere anche le classi compilate con warning ed errori mentre nella terza videata si indica se e come generare il file manifest del jar. In questa ultima terza videata del tool di generazione del jar, è possibile indicare la "main class" cioè indicare quale viene caricata se il jar viene "eseguito" come programma e non come libreria, cioè è possibile cercare tra tutte le classi selezionate una che abbia un main eseguibile così da rende il jar eseguibile.

Per Custom Tags sono degli elementi java (quindi server) definiti dall'utente utilizzabili nel codice delle pagine jsp, sono distribuiti dentro a delle librerie particolari dette TagLib che definiscono la "firma" del tag e anche la propria implementazione, l'uso di questi tag permette di separare il codice "client" da quello "server" con lo scopo di evitare di scrivere codice Java puro nelle pagine jsp e di superare i limiti della programmazione XHTML. In questo articolo vedremo solo come importare ed usare alcuni Tag, negli articoli successivi vedremo come implementare un nuovo tag e una nuova funzione.

Tutti i Custom tag hanno questa sintassi:

<prefix:tag attr1="value" ... attrN="value" />

oppure

<prefix:tag attr1="value" ... attrN="value" >Body</prefix:tag>

esattamente come un tag HTML normale o come i tag standard JSP. Per usare un custom tag dentro ad una jsp bisogna importgare la TagLib e poi definirla, all'interno di ogni TagLib è presente un file TLD che rappresenta le firme dei tag e definisce quali classi java implementano quel tag, è fondamentale conscere il TLD di una taglib.

Nel nostro esempio usiamo la taglib di Struts1 che ci servirà anche nei prossimi articoli, questa libreria può essere scaricata dal struts.apache.org dove possiamo selezionare la versione 1.3.10 e scaricare lo zip dove troveremo il jar che contiene la taglib: struts-taglib-1.3.10.jar. Se proviamo ad aprire il jar, per esempio decomprimerlo, noteremo che i file TLD sono all'interno della cartella META-INF e le classi dei custum tag sono nei corrispettivi package, come già visto basta prendere questo jar e posizionarlo nella nostra cartella lib per poter usare questa taglib. Nelle pagine jsp per poter usare le taglib appena importata, basta aggiungere la direttiva java all'inizio del file:

<%@ taglib uri="/tags/struts-bean" prefix="bean" %>

dove la proprietà uri da inserire in pagina è indicata nel file TLD della libreria mentre la proprietà prefix è un nome che permettera in pagina di richiamare il singolo custom tab, per esemnpio per usare il tag message si può usare il tag message:

<bean:message key="chiave" bundle="nomeBundle" />

Sviluppando applicazioni JEE si hanno a disposizione più modi di creare un custom-tag, uno dei metodi più classici è quello di usare una classe java e un TLD per la descrizione del tag, il primo passo è quello di creare un file TLD nella giusta cartella /progetto/WebContent/WEB-INF/tld/nome.tld, ricordandosi che ad un tld non corrisponde un solo tag ma corrisponde una libreria che può comprendere anche più tag, una documentazione completa dei file TLD può essere trovata in docs.oracle.com, lo scheletro base di questo tipo di file è:

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
    http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
  <description>Alnao tag library</description>
  <tlib-version>1.0</tlib-version>
  <short-name>alnao</short-name>
  <tag>
    <name>TagCommentoSenzaParametri</name>
    <tag-class>it.alnao.prova.tags.TagCommentoSenzaParametri</tag-class>
    <body-content>JSP</body-content>
  </tag>
  <tag>
    <description>Importo</description>
    <name>Importo</name>
    <tag-class>it.alnao.prova.tags.ImportoTag</tag-class>
    <body-content>JSP</body-content>
    <attribute>
      <name>positiveStyle</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
    <attribute>
      <name>negativeStyle</name>
      <required>true</required>
      <rtexprvalue>true</rtexprvalue>
    </attribute>
  </tag>
</taglib>

in questo file abbiamo definito la libreria assegnandogli anche uno short-name, e un elenco di tag dove, per ogni elemento, dobbiamo definire il nome, la classe e gli attributi se presenti. Poi ci serve definire la classe TagCommentoSenzaParametri che deve espandere la classe TagSupport, per esempio un tag java che scrive un commento in pagina è

package it.alnao.prova.tags;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.TagSupport;

public class TagCommentoSenzaParametri extends TagSupport {
 private static final long serialVersionUID = 1L;

 public int doStartTag() throws JspException {
   //HttpSession session = pageContext.getSession();
   //pageContext.getRequest().setAttribute("Nome", valore);
   String s="\n\n<!-- prova tag AlNao.it -->\n\n";
   try { 
     JspWriter out = pageContext.getOut();
     out.println(s); 
   } catch (Exception e) { 
     e.printStackTrace(); 
   } 
   return SKIP_BODY;
 }
 public int doEndTag() throws JspException {
   return EVAL_PAGE;
 }
}

mentre il tag per la visualizzazione di importi è:

package it.alnao.prova.tags;
 
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Locale;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.BodyTagSupport;

public class ImportoTag extends BodyTagSupport {
 private static final long serialVersionUID=1L;
 private String positiveStyle = null;
 private String negativeStyle = null;
 private String bodyTag = null;
 public int doStartTag() throws JspException {
   return EVAL_BODY_BUFFERED;
 }

 public int doAfterBody() throws JspException {
   bodyTag = getBodyContent().getString().trim();
   if (bodyTag != null) {
   Number decimal;
   DecimalFormat df = new DecimalFormat();
   decimal = df.parse(bodyTag);
   if (decimal != null) {
     if (decimal.doubleValue() > 0) { //positivo
       bodyTag = "<span class=\"" + getPositiveStyle() + "\">";//+ bodyTag
     } else {
       if (decimal.doubleValue() < 0) { //negativo
         bodyTag = "<span class=\"" + getNegativeStyle() + "\">";//+ bodyTag
       } else { //zero
         bodyTag = "<span class=\"defaultClass\">" ;//+ bodyTag
       }
   } else { //non è un numero
     bodyTag = "<span>";
 }
 return (SKIP_BODY);
 }

 public int doEndTag() throws JspException {
   bodyTag = bodyTag + "</span>";
   try {
     this.pageContext.getOut().print(this.bodyTag);
   } catch (IOException ioe) {
     throw new JspException(ioe);
   }
   return EVAL_PAGE;
 }
 ... metodi get e set delle proprietà ...
}

per richiamare questi tag nelle pagine bisogna prima di tutto importare la libreria TLD con il comando ad inizio jsp

<%@ taglib uri="/WEB-INF/tld/nome.tld" prefix="nomeTLD" %>

e poi usare il prefisso per chiamare i singoli tag, per esempio:

<nomeTLD:TagCommentoSenzaParametri />
<nomeTLD:Importo positiveStyle="classeCCS1" negativeStyle="classeCCS2">-12.12</nomeTLD:Importo>

Nelle applicazioni JavaEE è possibile creare un custom-tag con una classe java e, grazie alla tecnica dell'ereditarietà di Java, utilizzare classi già esistenti per evitare di scrivere codice già presente in altri tag, per fare questo basta estendere una classe già esistente, per esempio è possibile creare un tag if personalizzato che abbia due parametri in input, se sono uguali il corpo del tag verrà visualizzato in pagina, altrimenti il codice verrà saltato, il codice della classe dovrebbe essere:

import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.jstl.core.ConditionalTagSupport;
public class IfTag extends ConditionalTagSupport { 
 private String value1 = null;
 private String value2 = null;
 public IfTag() {
  super();
 }
 protected boolean condition() throws JspTagException {
  if (value1==null || value2==null)
   throw new JspTagException ("Parametri nulli");
  return value1.equals(value2);
 }
}

dopo aver definito il tld corrispettivo, in una qualsiasi pagina jsp poi basta usare il tag così:

<MieiTag:IfTag value1="uno" value2="due">
 <%
 //questo codice non viene eseguito perchè "uno" è diverso da "due"
 %>
<MieiTag:IfTag>
<MieiTag:IfTag value1="tre" value2="tre">
 <%
 //questo codice viene eseguito perchè le stringhe sono uguali
 %>
 Viene mostrato questo messaggio perchè "tre"=="tre"
</MieiTag:IfTag>

Allo stesso modo è possibile scrivere un tag "else" che estende la classe "if" in questo modo ma che nega il valore del metodo condition:

public class ElseTag extends IfTag{ 
  protected boolean condition() throws JspTagException {
    return ! super();
  }
}

nota dell'articolo: la classe ConditionalTagSupport si trova nella libreria jstl che verrà introdotta nei prossimi articoli.

Creare un custom-tag con una classe java è una tecnica molto elegante ma può risultare complicata se si deve scrivere un TAG che comprende molto codice client all'interno perché, per scrivere codice HTML dalla classe, si usa il metodo out che può risultato non semplicissimo da usare e il risultato rischia di diventare una classe molto lunga con molte stringhe costanti all'interno del codice, fortunatamente esiste un'altra tecnica per scrivere tag: usando un file jsp dedicato. Prima di tutto bisogna sempre definire un TLD e il riferimento al tag senza però nessun parametro anche se previsti, per esempio:
<tag-file>
 <name>tag_esempio</name>
 <path>/WEB-INF/tags/esempio.tag</path>
 </tag-file>
poi si crea il file, per convenzione ha estensione tag ma in realtà è un file jsp, questo file deve sempre iniziare con il meta-tag
<%@ tag %>
poi deve essere presente l'elenco degli attributi, cioè i parametri del tag, per esempio:
<%@ attribute name="oggetto" required="true" rtexprvalue="true" description="oggetto" %>
e dopo si inserisce il codice HTML/Java del tag come se fosse un file jsp, per esempio
<DIV class="classeDiv">
 L'oggetto ha nome <%=request.getParameter("oggetto")%>
</DIV>
Per richiamare il tag da qualsiasi pagina JSP basta invocarlo come già visto richiamando:
<tagLib:tag_esempio oggetto="valore" />
Scegliere tra custom-tag con una classe Java o con un file jsp è una scelta di gusti, tipicamente si usano i file jsp quando il codice HTML è tanto e il codice Java è poco, viceversa si usano le classi quando il codice Java è molto complicato oppure se si deve definire un tag che estende un altro tag.

Se si vuole distribuire o anche solo usare un proprio custom-tag scritto in java (o in una jsp) bisogna sempre definire un file TLD, che è un semplice file XML che definisce la struttura base di una libreria di custom-tag, la documentazione ufficiale dei TLD è disponibile al sito docs.oracle.com e per ogni tipo di tag c'è una struttura ben definita per la gestione delle caratteristiche del tag stesso. Oltre alle basi di ogni TLD dove bisogna definire una descrizione, una versione e un nome

 <description>MyWeb tag library</description>
 <tlib-version>1.0</tlib-version>
 <short-name>my</short-name>

Per i tag definiti in una classe bisogna definire un nome, il riferimento alle classi e il contenuto, per esempio

<tag>
 <name>SessionBean</name>
 <tag-class>it.alnao.example.tags.SessionBeanTag</tag-class>
 <body-content>JSP</body-content>
</tag>

Se poi il tag ha anche degli attributi bisogna definirli come elenco, specificando una descrizione, il nome e le regole di obbligatorietà, il rtexprvalue permette di filtrare le espressioni nelle proprietà:

<tag>
 <description>Caption</description>
 <name>caption</name>
 <tag-class>it.alnao.example.tags.CaptionTag</tag-class>
 <body-content>JSP</body-content>
 <attribute>
  <description>Message key</description>
  <name>key</name>
  <required>false</required>
  <rtexprvalue>true</rtexprvalue>
 </attribute>
 <attribute>
  <description>Localization context</description>
  <name>bundle</name>
  <required>false</required>
  <rtexprvalue>true</rtexprvalue>
 </attribute>
</tag>

Per quanto riguarda i tag definiti in pagine (jsp con estensione tag), la sintassi è quasi banale visto che non bisogna definire l'elenco degli attributi ma basta un nome e l'indirizzo del file .tag:

<tag-file>
 <name>d</name>
 <path>/WEB-INF/tags/bar/baz/d.tag</path>
</tag-file>

Quando si sviluppano tag per un singolo progetto questa operazione sembra banale ma la grande potenzialità di questi file è che possono essere inseriti dentro ad un jar e distribuiti assieme alla propria implementazione così si può distribuire una tag-lib con il proprio TLD con un solo file jar, ed è proprio quello che faremo con la tag-lib di struts, per jstl o per qualsiasi altra tag-lib studiata per JavaEE.

Durante la fase di sviluppo è fondamentale gestire e saper leggere i log scritti dall'applicazione sia per la fase di debug, sia per la fase di gestione dei dati, ma per fortuna le applicazioni basate sull'architettura Java EE mettono a disposizione "quasi" automaticamente la gestione dei log. Per questo nelle classi java si può benissimo usare lo standard output di java tramite System.out.println ma questo è sconsigliato perché è buona norma usare sempre la libreria log4j che è diventata uno standard per le applicazioni Java EE per il sistemi a livelli e perché è stata studiata per funzionare con qualsiasi tipo di WebServer, infatti è inglobata nei componenti di Apache ed è disponibile in tutti i server derivati compresi anche i proprietari di IBM (WebSphere) e Oracle (WebLogic) oltre che i free come Tomcat e Jboss. Log4j funziona a livelli cioè un programmatore può decidere a quale livello scrivere un log e il WebServer decide se scrivere o meno quel log a seconda della configurazione del log4j nell'applicazione, i livelli di configurazione sono:

  • OFF : Log completamente disattivati
  • FATAL: Errore importante che causa un prematuro termine dell'esecuzione.
  • ERROR : Un errore di esecuzione o una condizione imprevista.
  • WARN : Condizione inaspettata ma che non comporta un errore
  • INFO : Messaggio informativo
  • DEBUG : Usato in fase di sviluppo o in fase di testing
  • TRACE : Informazioni molto dettagliate

Per usare la libreria bisogna ovviamente includere la libreria cioè basta posiziona il jar della libreria, scaricabile dal sito ufficiale di Apache, e posizionarlo nella cartella WEB-INF/lib/ del proprio progetto, a questo punto bisogna creare un file di configurazione per il sistema di logging, per fare questo si può creare un file xml o un file di properties, per la prima scelta vi rimando alla documentazione ufficiale, per la seconda basta creare un file nella cartella src del progetto con il nome log4j.properties:

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, APPENDER_OUT, APPENDER_FILE
# APPENDER_OUT is set to be a ConsoleAppender.
log4j.appender.APPENDER_OUT=org.apache.log4j.ConsoleAppender
log4j.appender.APPENDER_OUT.layout=org.apache.log4j.PatternLayout
log4j.appender.APPENDER_OUT.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n
#APPENDER_FILE
log4j.appender.APPENDER_FILE=org.apache.log4j.RollingFileAppender
log4j.appender.APPENDER_FILE.File=mioLog.log
log4j.appender.APPENDER_FILE.MaxFileSize=100KB
log4j.appender.APPENDER_FILE.MaxBackupIndex=10
log4j.appender.APPENDER_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.APPENDER_FILE.layout.ConversionPattern=%p %t %c – %m%n
#Logger: Print messages up to level DEBUG/INFO/ERROR
log4j.logger.net.jawr=ERROR
log4j.logger.org.apache=WARNING
log4j.logger.net.sf.navigator.taglib.UseMenuDisplayerTag=ERROR
log4j.logger.net.sf.navigator.util.PropertyMessageResources=ERROR

Il file deve definire due parti: Appender e Logger, la prima riga definisce l'Appender e ci associa due elementi APPENDER_OUT e APPENDER_FILE (nell'esempio sono due ma ne basta anche uno solo), per ogni APPENDER si deve definire la classe delegata: ConsoleAppender permette di scrivere sulla console dell'applicazione di default mentre FileAppender permette di scrivere su file separato mentre RollingFileAppender funziona come FileAppender ma si definisce anche una grandezza massima del file e quando la lunghezza massima è raggiunta, il file è rinominato aggiungendo un numero progressivo al nome del file. In ogni Appender comunque bisogna definire anche un pattend con cui i log vengono scritti tramite la classe ConversionPattern, nell'esempio viene associato l'appender A1 allla classe ConsoleAppender, vi rimando alla documentazione ufficiale per i dettagli sui parametri.

La sezione di Logger invece permette di definire per dei sottopackage dei livelli particolari, questo risulta utile per esempio quando si vuole separare package architetturali dai package applicativi, nell'esempio il package di oracle scriverà sui log solo se lancia dei warning (o più gravi) mentre gli info e i debug saranno ignorati, ciò non vale per i package non specificati perchè useranno il livello di default, che è DEBUG.

In una classe qualsiasi classe java del progetto si possono usare le istruzioni della classe logger per scrivere nei vari livelli di log:

import org.apache.log4j.Logger;
...
 Logger log = Logger.getLogger(this.class);//definisce i log
 log.debug(" Livello DEBUG");
 log.info(" Livello INFO");
 log.warn(" Livello WARNING");
 log.error(" Livello ERROR", new Exception("descrizione exception"));
 log.fatal(" Livello FATAL");

Per quanto riguarda l'ambiente di sviluppo se si lancia il server dalla vista "Servers" di eclipse basta aprire la vista console per avere in tempo reale il log sotto mano mentre se si usa un WebServer esterno non agganciato ad eclipse bisogna trovare la cartella di log, di solito, negli ambienti GNU Linux tale cartella è dentro ad /var/log/.

Oltre alla gestione degli errori e dei log nelle classi bisogna anche prevedere e gestire gli errori nelle pagine jsp, questo perché un file jsp non è compilato in un file class ma viene compilato direttamente dal WebServer durante il run-time della pagina stessa quindi la gestione degli errori è completamente diversa. L'uso di un framework come struts permette delegare la gestione di questi errori ma la versione base di Java EE mette a disposizione la dispositiva errorPage, infatti in una qualsiasi pagina basta definire:

<%@ page errorPage = "errore.jsp" %>

e poi gestire l'oggetto exception creando la jsp

<html>
  <head></head>
  <body>
    <%@ page isErrorPage = "true" %>
    <div class="message">
      Siamo spiacenti, si è verificato un errore
      durante l'esecuzione:<br /><br />
      <%= exception.getMessage()%>
    </div>
  </body>
</html>

La componenti di collegamento con una base dati è fondamentale nelle applicazioni JavaEE ed è possibile trovare framework e kit vari per i vari collegamenti come Hibernate che vedremo più avanti, in questo articolo invece vedremo come usare le librerie base, cioè aprire una connessione con il DB ed esegurie una semplice query statica. Per prima cosa dobbiamo scaricare ed installare nella cartella WEB-INF/lib la libreria di mysql

mysql-connector-java-5.1.7-bin

oppure una versione più recente se disponibile, questa libreria ci permette di usare il connettore/driver com.mysql.jdbc.Driver che ci permetterà di collegarci ad un database di mysql, se invece volessimo collegarci ad un altro database, come oracle, ci basterà trovare ed installare il driver relativo. Un esempio di connessone al database e successiva operazione di select è:

 private final static String DRIVER = "com.mysql.jdbc.Driver";
 String sqlCommand="select nome, cognome from tabella where nome is not null order by cognome";
 Class.forName(DRIVER);
 Connection con = DriverManager.getConnection (getUrl(),getUser(),getPassword());
 PreparedStatement cmd = con.prepareStatement(sqlCommand);
 ResultSet res = cmd.executeQuery();
 cmd.close();
 con.close();

Un esempio di operazione di update, insert oppure delete è:

 String sqlCommand="update campo1='1' from tabella where campo1 is null";
 Class.forName(DRIVER);
 Connection con = DriverManager.getConnection (getUrl(),getUser(),getPassword());
 PreparedStatement cmd = con.prepareStatement(sqlCommand);
 int i=cmd.executeUpdate();
 cmd.close();
 con.close();

Da notare che Connection,PreparedStatement e ResultSet, sono tutte classi del package standard java java.sql che non dipendono dal singolo database quindi è possibile sviluppare indipendentemente dal tipo di base dati, nel caso delle select per esempio il cursore di ritorno è di tipo java.sql.ResultSet, per gli oggetti di questo tipo si può eseguire un ciclo per il recupero dei dati come se fosse un cursore nella tabella sql:

 ArrayList<String> nomi=new ArrayList<String>();
 if (rs!=null){
   while (rs.next()) {
     nomi.add( rs.getString("nome") );
   }
 }

Per la documentazione ufficiale di questo package vi rimando alla guida ufficiale e in particolare la pagine sulla classe ResultSet che risulta la più usata in questo tipo di applicazioni.

Da notare che nell'esempio è stato utilizzato il DRIVER del database MySql ma è possibile utilizzare gli stessi oggetti nel package java.sql anche se si usa un database di un altro tipo, per esempio Oracle con il driver oracle.jdbc.OracleDriver.

La scrittura delle query direttamente le codice è un sistema sconsigliata ed è considerata una tecnica deprecata visto in quanto esistono modi più moderni per eseguire le query e le istruzioni SQL: oltre ai vari framework, come Hibernate, è possibile utilizzare la classe PreparedStatement del package java.sql, infatti questa tecnica evoluta permette di scrivere una semplice query indicando una lista di parametri e poi valorizzare un array, per esempio ipotizzando di avere una tabella con tre campi si può pensare una insert con l'istruzione:

insert into tabella (nome,descrizione,ordine) VALUES (?,?,?)

e il codice java diventa

public static final String K_INSERT_TABELLA="insert into tabella (nome,descrizione,ordine) VALUES (?,?,?)";
..
int numeroParametro=0;
Connection con = DriverManager.getConnection (getUrl(),getUser(),getPassword());
PreparedStatement cmd = con.prepareStatement(K_INSERT_TABELLA);
cmd.setString(numeroParametro++,"Alberto"); //imposto il primo parametro
cmd.setString(numeroParametro++,"il mio nome"); //imposto il secondo parametro
cmd.setInt(numeroParametro++,123); //imposto il terzo parametro
int result=cmd.executeUpdate();
cmd.close();
con.close();
...

Lo stesso esempio usando una istruzione sql di tipo select:

public static final String K_INSERT_TABELLA="select * from tabella where nome = ? ";
...
int numeroParametro=0;
Class.forName(DRIVER);
Connection con = DriverManager.getConnection (getUrl(),getUser(),getPassword());
PreparedStatement cmd = con.prepareStatement(K_INSERT_TABELLA);
cmd.setString (numeroParametro++, "Alberto");
ResultSet res = cmd.executeQuery();
cmd.close();
con.close();
...

da notare che il primo parametri di setString o setInt è la posizione del parametro all'interno della query. Questa tecnica poi viene sorpassata e deprecata dai vari gestori di database più evoluti come Hibernate che vedremo nei prossimi articoli ma è sempre buona norma conoscere almeno questa tecnica per sapere come agire in caso di necessità.

La struttura del framework di struts è semplice: tutto si basa su uno (o più file) chiamati struts-config che definisce le "azioni" e i dati per la configurazione contenuti nel file vengono letti in fase di avvio dell'applicazione, il flusso del framework funziona che quando un client invia una richiesta HTTP, la richiesta viene ricevuta dalla servlet di Struts che provvede a popolare un oggetto di tipo ActionForm associato alla richiesta popolato automaticamente con i dati della request e un secondo oggetto di tipo ActionMapping associato alla richiesta stessa, la servlet generale delega l'elaborazione dei dati alla relativa classe Action passandole in input gli oggetti con request e response HTTP, l'ActionForm e l'ActionMapping precedentemente valorizzati, la action compie la logica di business e al termine dell'elaborazione restituisce alla ActionServlet un oggetto di tipo ActionForward contenente il path della vista da fornire all'utente cioè la jsp di risposta che viene processata e caricaa dal webserver.

Ogni Action, come abbiamo già detto, deve essere dichiarata e configurata nel file struts-config.xml e non nel web.xml anche se si tratta di una servlet, di default i nomi devono terminare con un suffisso .do ma è possibile specificare un suffisso personalizzato, tale suffisso permette alla richiesta di essere processata da Struts e non come una semplice servlet Java infatti (teoricamente) è possibile gestire le action Struts e servlet non del framework assieme ma non è consigliato usare.

Come azione preliminare per l'uso di struts dobbiamo censire nel file web.xml la servlet madre che poi tutte le action andranno ad estendere, aggiungendo la definizione:

<servlet>
  <servlet-name>action</servlet-name>
  <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
  <init-param>
    <param-name>config</param-name>
    <param-value>
      /config/struts-config.xml,
      /config/struts-secondoFile.xml,
      /config/struts-altroFile.xml
    </param-value>
  </init-param>
  <load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>action</servlet-name>
  <url-pattern>*.do</url-pattern>
</servlet-mapping>

Da notare che sono stati censiti più file xml come elenco separato da virgole e nel mapping è stato definito un pattern, cioè tutte le richieste HTTP che arriveranno al server con quel pattern verranno eseguite dalle servlet di struts, di default si usa il ".do" come suffisso ma è possibile modificarlo a proprio piacimento, è possibile anche definire più pattern anche se è ovviamente sconsigliato. Il file struts-config.xml deve essere posizionato dentro ad una cartella config e contiene varie configurazioni della struttura, in particolare bisogna censire tutte le classi di tipo ActionForm usate nelle action e devono essere censite tutte le action con il relativo url nella proprietà path, la classe nel campo type e il nome dell'actionForm nel campo name (da non confondere il name dell'action form con il path che rappresenta il "nome" della action); altre configurazioni sono possibili all'interno dello struts-config e verranno introdotte nei prossimi articoli, un semplice esempio di struts-config è:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE struts-config PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 1.1//EN" "http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
  <form-beans>
    <form-bean name="ProvaFormName" type="it.alnao.struts1.forms.ProvaClasseForm" />
  </form-beans>
  <action-mappings>
    <action path="/Prova" 
         type="it.alnao.struts1.actions.ProvaClasseAction" 
         name="ProvaFormName" >
       <forward name="success" path="/paginaDiProva.jsp" />
    </action>
  </action-mappings>
</struts-config>

Per usare struts in un progetto Java EE bisogna scaricare le librerie, per la versione che usamo per questa guida è la 1.30 ancora disponibile nel sito ufficiale https://struts.apache.org/download.cgi selezionando "Struts 1.3.10 Full Distribution", una volta scaricato lo zip e decompresso, si trova una cartella lib con dentro un elenco di jar:

  • antlr-2.7.2.jar
  • bsf-2.3.0.jar
  • commons-beanutils-1.8.0.jar
  • commons-chain-1.2.jar
  • commons-digester-1.8.jar
  • commons-fileupload-1.1.1.jar
  • commons-io-1.1.jar
  • commons-logging-1.0.4.jar
  • commons-validator-1.3.1.jar
  • jstl-1.0.2.jar
  • oro-2.0.8.jar
  • standard-1.0.6.jar
  • struts-core-1.3.10.jar
  • struts-el-1.3.10.jar
  • struts-extras-1.3.10.jar
  • struts-faces-1.3.10.jar
  • struts-mailreader-dao-1.3.10.jar
  • struts-scripting-1.3.10.jar
  • struts-taglib-1.3.10.jar
  • struts-tiles-1.3.10.jar

conviene importare tutti questi jar nel proprio progetto anche se non tutti verranno effettivamente usati, per evitare problemi di dipendenze e perché si ha già a disposizione la libreria completa, compresa la libreria jstl (che vedremo in successivi articoli) e tutte le librerie base di apache sempre molto utili.

In tutte le versioni di Struts, l'entità principale è la action definita nel file di configurazione principale, la action rappresenta una richiesta al webserver tramite una URL, il framework esegue la servlet corrispondente alla action e poi esegue il forward verso la pagina jsp censita nel file di configurazione, il censimento delle action nel file di configurazione ha alcune regole:

  • il nome della action deve essere censito nella proprietà "path" e deve iniziare con il carattere / (barra retroversa), questo nome è univoco all'interno dell'applicazione, se ci dovessero essere più action con lo stesso nome, il framework non saprebbe cosa fare, all'interno dei nomi possono essere usati i caratteri alfanumerici e il carattere _ (underscore) ma non i caratteri speciali
  • il riferimento alla classe della servlet deve essere censito nella proprietà type
  • il riferimento al actionForm censito nella sezione form-bean dello struts-config, deve essere censito nella proprietà name, da non confondere il name dell'actionForm con il nome della action
  • come figlio del tag <action> ci sono i vari forward che vanno censiti come name (un nome) e il path che inizia sempre con il carattere / (barra retroversa)
Un esempio di censimento di una action
<action 
    path="/Prova" 
    type="it.alnao.strtus.example.actions.ProvaClasseAction" 
    name="ProvaFormName" >
  <forward name="success" path="/paginaDiProva.jsp" />
</action>
La classe della servlet estendere la classe Action (org.apache.struts.action.Action) e, di base, deve avere un metodo excecute con in input il mapping, il form, request e response e ritornare un oggetto di tipo ActionForward, nell'esempio:
package it.alnao.strtus.example.actions;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
public class ProvaClasseAction extends Action {
  private static final long serialVersionUID = 1L;
  Logger log = Logger.getLogger("ProvaClasseAction");
  
  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws Exception {
    log.debug("metodo execute ");
    request.setAttribute("NomeParamtro","valore"); //esempio
    return mapping.findForward("success");
  }
}

Ora basta creare la nostra pagina jsp che deve chiamarsi come definito nel file di configurazione paginaDiProva.jsp, per esempio:

<html>
<head></head>
<body>
<%=request.getAttribute("NomeParamtro"); %>
</body>
</html>

Nelle application con Struts è possibile utilizzare la stessa classe per eseguire action diverse per evitare di definire tantissime classi e tantissime definizioni delle action nel file di configurazione: è possibile infatti definire una singola classe con una sola action nel file di configurazione con all'interno del tag e si possono definire più metodi per ciascuna sotto-azione logica all'interno della action.

Per utilizzare questa tenicnica basta aggiungere un parametro di nome method alle action che sarà usato dalla classe architetturale DispatchAction per determinare quale metodo invocare, la classe dell'applicazione deve estendere la classe DispatchAction, per esempio:

public class UserDispatchAction extends DispatchAction {
  public ActionForward remove(...) throws Exception { 
    System.out.println("REMOVE USER!!!!");
    return mapping.findForward("success");
  }
  public ActionForward save(...) throws Exception { 
    System.out.println("SAVE USER!!!!");
    return mapping.findForward("success");
  }
}

Da notare che tutti i metodi hanno la stessa firma del metodo Action.execute() mentre nel file di configurazione si usa il parameter per indicare il parametro da usare per indicare quale metodo usare nella classe action.

<action path="/UserDispatchAction" type="UserDispatchAction" parameter="method" >
  <forward name="success" path="/success.jsp" />
</action>

Quindi poi per chiamare i metodi si usano gli url

UserDispatchAction.do?method=remove
UserDispatchAction.do?method=save

Questa tecnica è usata proprio per evitare di scrivere molti classi ma avere poche classi con più metodi execute al proprio interno però ovviamente il programmatore non dovrebbe abusare di questa tecnica ed utilizzare una unica action per cose non legate tra loro.

Una delle caratteristiche principali di struts è che definisce i forward, cioè ogni classe/metodo action termina con un forward in una pagina jsp con la caratteristica che la request nella action viene poi passata alla jsp in modo trasparente all'utente. E' possibile definire più forward per una action e gestire la scelta dei forward direttamente all'interno della classe action, per esempio:

<action path="/nomeAction" type="it.alnao.struts.test.Nomeaction " >
  <forward name="forward_ok" path="/jsp/ok.jsp"/>
  <forward name="forward_ko" path="/jsp/ko.jsp"/>
</action>

E la classe

public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) {
  Boolean condizione=true;
  if (condizione){
    return mapping.findForward("forward_ok");
  }else{
    return mapping.findForward("forward_ko");
  }
}

In questo modo è possibile definire una sola action con più forward, per esempio con il metodo del dispatch action è possibile definire una sola classe action, con più metodo e più jsp di forward ma ricordo sempre che è buona norma non esagerare e di raggruppare in una singola action solo quei metodi/jsp inerenti tra loro.

Il framework Struts mette a disposizione un componente e una taglib per la gestione dei messaggi e del multilingua per le applicazioni, la filosofia base è la creazione di uno o più file di properties di tipo chiave=valore, dove le chiavi sono un nome del messaggio e il valore è la label o il messaggio da mostrare in pagina, in questo modo è possibile avere tutte le label e messaggi in un unico file e nelle pagine si usa un tab per leggere il valore di una specifica chiave dal file di properties, Struts poi permette anche di creare più file specifici diversi per lingua e, a seconda della lingua specificata, il framework seleziona il file e la chiave corrispondete.

Per fare questo bisogna creare all'interno del progetto su Eclipse, nella cartella src del progetto un file di properties, per esempio AlNaoMessageResources.properties, all'interno di questo file ogni riga è una corrispondenza di chiave=valore, per esempio

#Con il carattere cancelletto si possono inserire dei commenti all'interno dei file
MessaggioBenvenutoTitolo=Benvenuto 
MessaggioBenvenutoTesto=Benvenuto utente nel sito

ma non si può andare a capo, questa non è un valore

MessaggioBenvenutoFooter= (c) copyright footer

nel file struts-config bisogna ricordarsi di censire le risorse in modo che il framework carichi il file all'avvio del server

<message-resources parameter="AlNaoMessageResources" />

nelle jsp si può leggere i valori delle properties, per esempio una jsp:

<%@ taglib uri="/tags/struts-bean" prefix="bean" %>
<html>
  <head>
    <title><bean:message key="MessaggioBenvenutoTitolo"/></title>
  </head>
  <body>
    <p><bean:message key="MessaggioBenvenutoTesto"/></p>
    <p><bean:message key="MessaggioBenvenutoFooter"/></p>
  </body>
</html>

oppure è anche possibile leggere i valori dai file di properties con un semplice codice java, per esempio un metodo statico per il recupero dei valori:

public static String getStringFromResource(String bundleName,String key) {//BRASIL
  if(key == null) return null;
  String value = "???";
  try {
    ResourceBundle resource=ResourceBundle.getBundle(bundleName,Locale.getDefault());/*, Locale.getDefault()*/ /*new Locale("")*/
    value = resource.getString(key);
  } catch (MissingResourceException ex) {
    value += key;
  }
  return value;
}

e richiamare questo metodo con

NomeClasse.getStringFromResource("AlNaoMessageResources","MessaggioBenvenutoTesto");

da notare che questa tecnica usa i componenti standard ResourceBundle e MessagesResources di Struts e Apache. Per rendere i Bundle multipli per lingua è necessario creare più file, per esempio creando file

AlNaoMessageResources.properties 
AlNaoMessageResources_it_IT.properties
AlNaoMessageResources_en_GB.properties

dove il primo file contiene i valori di default, il secondo le traduzioni in italiano mentre il terzo le traduzioni in lingua inglese, per impostare la lingua della sessione di un utente basta salvare in sessione un attributo con la lingua, per esempio una semplice action:

import java.util.Locale;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.actions.DispatchAction;

public class LanguageSelectAction extends DispatchAction{
  public ActionForward chinese(ActionMapping mapping,ActionForm form, HttpServletRequest request,HttpServletResponse response) throws Exception {
    Locale.setDefault(Locale.ITALIAN); 
    return mapping.findForward("success");
  }
  public ActionForward english(ActionMapping mapping,ActionForm form, HttpServletRequest request,HttpServletResponse response) throws Exception {
    Locale.setDefault(Locale.ENGLISH);
    return mapping.findForward("success");
  }
}

e nel web.xml è possibile configurare il linguaggio di default con:

<context-param>
<param-name>LOCALE</param-name>
<param-value>en-GB</param-value>
</context-param>

E' anche possibile creare più Bundle diversi all'interno dello stesso progetto ed è possibile posizione i file di properties assieme ai package java, per esempio posizionando un file nel package

\src\it\alnao\struts1\prova\AlNaoMessageResources.properties

basterà censire il file nello struts-config

<message-resources parameter="it.alnao.struts1.prova.AlNaoMessageResources" key="AlNaoMessageResources"/>

Oltre ai componenti per le action e i forward, Struts1 mette a disposizione una serie di librerie taglib per la gestione dei bean e dei form all'interno delle pagine jsp, per l'import nelle jsp di queste taglib (TLD) è necessario usare

<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>

I tag generici per la formattazione e la navigazione all'interno della libreria HTML sono:

  • html Visualizza un elemento html
  • base Realizza l’elemento base di HTML: definisce in automatico l’indirizzo di base per i link relativi
  • link Crea il codice HTML per il classico link, ma possiamo sfruttarlo per collegare anche le nostre action, che il framework interpreta come ActionForward
  • img Visualizza un tag HTML img
  • frame Definisce un frameset HTML, le pagine vengono chiamate al suo interno con lo stesso meccanismo del tag <html:link>
  • errors Serve per mostrare messaggi d’errore, ad esempio in caso di validazione di un form

Per esempio

<html:html locale="true">
</html:html>

Mentre per i form ci sono a disposizione i tag

  • form Definisce un form HTML
  • text Casella di testo
  • password Visualizza un campo di input per le password
  • textarea Visualizza un campo di input textarea
  • option Crea un menu a tendina
  • checkbox Visualizza un campo di input (checkbox)
  • radio Visualizza un radio button
  • submit Visualizza un pulsante submit
  • cancel Crea un campo di input che annulla il form
  • reset Visualizza un campo di input con pulsante reset
  • button Crea un campo di input (pulsante)
  • file Visualizza un campo di input per la selezione di un file
  • image Visualizza un tag input del tipo image
  • hidden Genera un campo nascosto
  • multibox Mostra un elenco di checkbox
  • options e optionsCollection Visualizzano una raccolta di opzioni in una semplice select

Il tag <html:form> di Struts è uno dei più importanti della HTML taglib visto che permette di gestire i form automaticamente con struts e che userà il relativo ActionForm per memorizzare i dati. Gli attributi più importanti sono:

  • action Obbligatorio e deve essere dichiarato l’URL che riceverà i dati da questo form, deve essere una action struts censtita nel file di configurazione
  • method Il metodo HTTP per la richiesta "POST" oppure "GET"
  • focus Il nome del campo a cui sarà assegnata la priorità nell’esame della pagina
  • style Lo stile CSS da applicare al form
  • enctype Esprime la codifica del contenuto da usare quando questo form viene passato all’elaborazione

Per quanto riguarda gli action form e la gestione dei tag verrà scritto un articolo dedicato

Nell'articolo della gestione del multilingua è stato introdotto brevemente il tag bean della tag-lib di struts, che permette di gestire i file di label e il multilingua di struts, in realtà questa tab-lib permette di usare altri tag molto utili tra cui il il bean:define e bean:write che servono per dichiarare e visualizzare una variabile, questi tag sono definiti con l'obbiettivo di limitare il più possibile l'uso delle scriptlet nelle pagine jsp, separando il codice java dal codice descrittivo delle pagine. Per esempio il tag define sostituisce a tutti gli effetti le dichiarazioni java, cioè il codice:

<% String uid="admin" ; %>

può essere sostituito con il tag define:

<bean:define id="uid" value="admin" type="java.lang.String" toScope="page" />

anche se apparentemente viene scritto più codice, le pagine jsp risulteranno molto più leggibili e il codice molto più elegante, utilizzando la proprietà toScope del tag è possibile accedere e salvare valori in "request" o in "session", per esempio:

<bean:define id="uid" value="admin" type="java.lang.String" toScope="session" />

Altro tag molto utile della libreria è il tag write, questo sostituisce il comando out.write e la (barbaria) del <%= variabile%>, infatti permette di inviare al client un valore server, per esempio

<bean:write name=”uid” />

anche in questo caso è possibile utilizzare la proprietà scope per leggere il valore da request o dalla session, inoltre è possibile accedere ad una proprietà di un oggetto, per esempio è possibile usare

<bean:write name="user" property="name" scope="session" />

per accedere alla proprietà name di un oggetto cliente presente in sessione, se l'oggetto è precedentemente salvato in sessione e se nella classe dell'oggetto user è definito il metodo pubblico getName(). Il tag write permette di formattare con la proprietà format il risultato come un numero, un importo, ecc..., è possibile anche usare la proprietà ignore="true" per evitare che il tag vada in errore se l'oggetto cercato non viene trovato, usando questa proprietà con un oggetto non esistente non visualizzerà nulla; le proprietà Locale e Bundle sono utilizzabili per la localizzazione il multilingua.

Il framework Struts è basato su java ed è molto usato il metodo delle estensioni delle classi, cioè in ogni applicazione è possibile definire una classe madre per esempio quella vista in "la prima action" e poi creare altre classi, figlie che estendono la prima classe:

public class SecondaClasseAction extends ProvaClasseAction {
  private static final long serialVersionUID = 1L;
  public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response) throws Exception 
   {
     super.execute(mapping,form,request,response);
     return mapping.findForward("success");
   }
}

in questo modo è possibile sfruttare l'ereditarietà di Java per gestire parti comuni delle applicazione in un unico punto, come la gestione dei log, la gestione della sessione o delle utenze.

Un caso di applicazione di questa tecnica è la gestione dell'auditing: il salvataggio di un log dove è indicata la lista delle operazioni eseguite di un utente in un file o in una tabella di un database, in struts esistono vari metodi: l'utilizzo di un filtro/interceptor può essere una soluzione, soluzione molto più semplice da realizzare è l'utilizzo di una classe madre con un metodo che salva il log e tutte le classi utilizzate nella applicazione devono richiamare quel metodo. Altro caso è la gestione del collegamento al database, è possibile definire i metodi di collegamento alla base di dati nella classe madre e poi le classi figlio usano il metodo del padre anche se in questo caso è consigliata la creazione di una classe "di utilità" separata.

Agli inizi degli anni 2000, quando venne rilasciata la prima versione di struts, tutti ritenevano il concetto di action-form molto rivoluzionario ma quasi inutile, il concetto di base degli action form è creare uno standard per la gestione delle valorizzazione dei dati inviati dai form al server e, nello stesso punto, validare il contenuto arrivato. L'utilizzo di questa tecnica delega al framework il recupero dei dati dal form e le valorizzazioni dei campi mentre al programmatore rimane il compito di definire la forma dei dati come un bean e il metodo di validazione dei dati, nella classe action poi il programmatore riceve l'oggetto di tipo action form già valorizzato, questa tecnica evita al programmatore di dover recuperare i valori dal form nella action.

Vediamo un esempio base di questa tenica: 1) si definisce una classe action-form con il metodo validate per ora vuoto

import org.apache.struts.action.ActionForm;
public class PersonaActionForm extends ActionForm {
  private nome String;
  private cognome String;
  private dataNascita String;

  public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
    ActionErrors errors = new ActionErrors();
    //TODO
    return errors;
  }
  public String getNome() { return nome; }
  public void setNome(String nome) { this.nome = nome; }
  public String getCognome() { return cognome; }
  public void setCognome(String cognome) { this.cognome = cognome; }
  public String getDataNascita() { return dataNascita; }
  public void setDataNascita(String dataNascita) { this.dataNascita = dataNascita; }
}

2) nel file di configurazione di struts è presente un tag specifico dove bisogna censire tutte le classi action-form usate

<form-beans>
  <form-bean type="it.alnao.j2ee.forms.PersonaActionForm" name="PersonaActionForm"/>
</form-beans>

3) nel file di configurazione di struts, per ogni action, è possibile aggiungere name e input, due proprietà usate dagli action form: name corrisponde al name dell'action form (nel tag form-beans) mentre input definisce l'action di destinazione in caso di errore nelle validazioni del form stesso

<action path="/PersonaSalvaAction" type="it.alnao.j2ee.action.PersonaSalvaAction" 
name="PersonaActionForm" input="/PersonaEditForm.do" scope="request">
  <forward name="success" path="/PersonaElencoAction.do" redirect="true"/>
</action>

4) nel form della pagina di edit (PersonaEditForm.do), si usano i tag di struts per definire il form e i var tag di input, è fondamentale che i campi property dei tag html:text corrispondano alle proprietà della classe dell'action-form, in caso di non corrispondenza ci sarà un runtime-error in fase di caricamento della jsp

<html:errors />
<html:form action="PersonaSalvaAction.do">
<p>
Nome : 
<html:text errorStyleClass="error" property="nome" styleId="nome" size="20" maxlength="100"/>
</p>
<p>
Cognome : 
<html:text errorStyleClass="error" property="cognome" styleId="cognome" size="20" maxlength="100"/>
</p>
<p>
DataNascita : 
<html:text errorStyleClass="error" property="dataNascita" styleId="dataNascita" size="10" maxlength="10"/>
</p>
<p> 
<button type="submit" class="buttonLightness2 ">Salva</button>
</p>
</html:form>

5) definizione della classe action dove è possibile usare l'oggetto del form con tutte le proprietà

public class PersonaSalvaAction extends ProvaClasseAction {
  ...
  public ActionForward execute(ActionMapping mapping, ActionForm form,
     HttpServletRequest request, HttpServletResponse response) throws Exception 
  {
     super.execute(mapping,form,request,response);
     PersonaActionForm personaActionForm = (PersonaActionForm) form; //cast dal form in input della action al tipo corrispondente
     if (personaActionForm == null){
       //gestione del caso null 
     }
     String nome=personaActionForm.getNome(); // possibile recuperare i dati inseriti dall'oggetto form senza usare l'oggetto request
     ...
     return mapping.findForward("success");
  }
  ...
}
La potenzialità dell'uso gli action form in pratica porta i vantaggi:
  • nel file di configurazione, per ogni action, è possibile definire un oggetto action form corrispondente
  • nelle pagine jsp, si usano i tag struts specifici html:form e html:text (come tutti gli html:input presenti) per evitare che in pagina ci siano input non presenti nella classe action-form
  • definizione di un metodo di validazione che viene eseguito lato server (e non lato client quindi non modificabile dall'utente)
  • gestione degli errori con messaggi e possibilità di gestire graficamente gli errori
  • nella classe action NON si usa il metodo request.getParameter per recuperare i valori inseriti ma si usano i metodi get della classe action-form
Nei successivi articoli verranno esposte le tecniche di validazione dei dati, la visualizzazione degli errori, l'uso della sessione nelle classi form e la creazione di oggetti con dati prima dell'ingresso della pagina con il form.

La struttura delle classi action form permette la definizione di un metodo validate dove eseguire i controlli sui dati inseriti prima che questi vengano inviati all'action, questo metodo viene eseguito prima della action e in caso di errore non viene eseguita la action di destinazione ma viene eseguito il forward nella action indicata nel file di configurazione nella proprietà input, per esempio:

<action path="/PersonaSalvaAction" type="it.alnao.j2ee.action.PersonaSalvaAction" 
name="PersonaActionForm" input="/PersonaEditForm.do" scope="request">
  <forward name="success" path="/PersonaElencoAction.do" redirect="true"/>
</action>

in caso di errore nella validazione viene eseguito il forward a PersonaEditForm.do, se invece non c'è nessun errore viene eseguita la classe e il metodo della action. Il metodo di validazione nella classe form ha in input il mapping della action e la request http, come valore di ritorno un oggetto ActionErrors contenente l'elenco degli eventuali errori delle validazioni. Un esempio del metodo:

public ActionErrors validate(ActionMapping mapping,HttpServletRequest request) {
  ActionErrors errors = new ActionErrors();
  if (StringUtils.isBlank(nome)){
    errors.add("nome", new ActionMessage("LabelNomeVuoto"));
  }
  if (StringUtils.isBlank(cognome)){
    errors.add("cognome", new ActionMessage("LabelCognomeVuoto"));
  }
  if (StringUtils.isBlank(dataNascita)){
    errors.add("dataNascita", new ActionMessage("LabelDataVuota"));
  }else{
    if ( ClasseUtilita.isValidData(dataNascita)){
      errors.add("dataNascita", new ActionMessage("LabelDataNonValida"));
    }
  }
  return errors;
}

All'interno del metodo sono presenti tutte le validazioni come il controllo dei campi obbligatori, formato delle date e formato dei numeri. Per esempio è possibile verificare che, in un campo numerico, l'utente abbia inserito un numero senza caratteri Per inserire un errore nello stack degli errori si usa il metodo

errors.add("nomeCampo", new ActionMessage("LabelDiErroreDaVisualizzare"));

Dove il primo parametro è il nome della proprietà dove l'errore si è verificato, così al ritorno nella pagina di errore è possibile "evidenziare" l'input della pagina con la proprietà

errorStyleClass="classeDiErrore"

del tag html:text in caso di errore di quell'input al tag verrà aggiunta la classe css indicata nella proprietà.

Nel secondo parametro viene indicato il messaggio di errore configurato nei file di properties e visualizzato in pagina nel tag

<html:errors />

che elenca gli errori in una lista UL/LI, è anche possibile personalizzare la grafica del tag creando delle properties specifiche, di default le properties sono importate con

#Properties di default del tag <html:errors />
errors.header=<UL>
errors.footer=</UL>
errors.prefix=<LI>
errors.suffix=</LI>

ma è possibile modificarle anche con i tag DIV

errors.header=<div class\="msgRed msgError pad4">
errors.footer=</div>
errors.prefix=-&nbsp;
errors.suffix=<br/>

Da tenere sempre a mente che gli errori di validazioni non sono considerati delle exception java ma semplicemente una lista di oggetti, quindi non saranno visibili nello stack dei log o nei file di log del webserver.

L'uso degli action form è molto utile quando si vuole creare applicazioni complesse con molte pagine di input, l'utilizzo diventa indispensabile infatti quando ci sono molti pagine all'interno di un progetto.

Essendo classi java, è possibile utilizzare la tecnica della estensione per ri-utilizzare campi e validazioni di campi comuni, per esempio avendo un form "Persona" con le validazioni su nome e cognome, è possibile creare un form "Studente" che estende "Persona" ereditando così le validazioni sui campi base, ovviamente Persona eredita le proprietà di Persona quindi tutti i suoi campi.

Altra tecnica molto usata è la possibilità di accedere a request e sessione all'interno del metodo validate, grazie al parametro request in input al metodo è possibile accedere a tutti gli ottetti in HttpRequest e nella HttpSession, per esempio salvando in precedenza un valore e poi confrontandolo con quello inserito dall'utente come un OTP o un valore massimo.

Il sistema degli action form poi permette di rendere gli action form non volatili: se nel file di configurazione viene indicato request come scope dell'oggetto, questo viene salvato in request ad ogni submit e poi perso se viene eseguita un'altra action struts o un'altra request http, mentre è possibile indicare la sessione come scope, in questo modo al submit del form, l'oggetto verrà salvato in sessione e recuperabile in action successive, queste tecnica è molto utile se lo stesso oggetto form è utilizzato in più pagine successive, per esempio un form di registrazione a più passi dove i dati inseriti dall'utente al primo step deve essere ricordato anche negli step successivi.

Sempre utilizzando l'architettura di struts, è possibile creare un oggetto form e salvarlo in sessione per poi atterrare in una pagina con i campi già valorizzati, per esempio partendo da un elenco di persone, cliccando su una riga è possibile chiamare una classe action che crea un oggetto form con i campi e poi questo oggetto può essere salvato in sessione con il nome indicato nella action nella proprietà "name", esempio di codice

1) configurazione delle azioni

<action path="/PersonaEditForm" type="it.alnao.j2ee.action.PersonaEditForm" >
  <forward name="success" path="/persona_edit.jsp"/>
</action>
<action path="/PersonaSalvaAction" type="it.alnao.j2ee.action.PersonaSalvaAction" name="PersonaActionForm" input="/PersonaEditForm.do" scope="session">
  <forward name="success" path="/PersonaElencoAction.do" redirect="true"/>
</action>

2) la classe PersonaEditForm

public class PersonaEditForm extends ProvaClasseAction {
  ...
  public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
    super.execute(mapping,form,request,response);
    String indicePersona=request.getParameter("indicePersona"); //recupero l'indice i-esimo passato dalla pagina di elenco
    Persona personaSelezionata=elencoPersoneInSessione.getPersona(indicePersona);//prendo dall'oggetto elencopersone l'iesimo elemento
    PersonaActionForm oggettoPersonaActionForm = new PersonaActionForm(); //creo un oggetto di tipo action form
    oggettoPersonaActionForm.setNome( personaSelezionata.getNome() );
    oggettoPersonaActionForm.setCognome( personaSelezionata.getCognome() );
    request.getSession().setAttribute("PersonaActionForm",oggettoPersonaActionForm); //salvo l'oggetto action form in sessione con il nome del form
    //importante che il nome salvato in sessione corrisponda alla proprietà name indicata nella action PersonaSalvaAction nel file di configurazione
    return mapping.findForward("success");
  }
  ...
}

3) nella pagina usiamo i tag html:form e html:input

<html:form action="PersonaSalvaAction.do">
<p> Nome : 
<html:text errorStyleClass="error" property="nome" styleId="nome" size="20" maxlength="100"/>
</p>
<p> Cognome : 
<html:text errorStyleClass="error" property="cognome" styleId="cognome" size="20" maxlength="100"/>
</p>
...

se tutti i nomi corrispondo, i tag di struts prenderanno dall'oggetto salvato in sessione i valori delle proprietà e valorizzerà i campi di input della jsp appena creata.

La libreria JSTL (Java Standard Tag Library) è stata un tentativo ben riuscito di creare uno standard per le applicazioni basate su Struts1 (e non solo), lo scopo di tale libreria è il NON utilizzo delle scriplet e del codice java nelle jsp, la libreria dei tag di struts infatti non è sufficiente per questo motivo, basti pensare agli elenchi dove è necessario un sistema di iterazione (clicli su un elenco) o il dover legare parti di pagina a condizioni (if su un elemento).

Per importare la libreria nella propria applicazione web basta posizionare il jar di jstl nella WEB-INF del proprio progetto (o nel classpath se si usano web-server avanzati), la libreria si divide in 5 componenti: Core, Formatting, Sql, Xml e Funzioni, ogni sottolibreria è in realtà una taglib a se quindi ogni componente deve essere importata separatamente nelle pagine jsp, per esempio:

<%@ taglib prefix = "c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix = "sql" uri="http://java.sun.com/jsp/jstl/sql" %>
<%@ taglib prefix = "x" uri="http://java.sun.com/jsp/jstl/xml" %>
<%@ taglib prefix = "fn" uri="http://java.sun.com/jsp/jstl/functions" %>

La sezione core è quella più usata perché permette, tra i tanti tag a disposizione, di manipolare variabili e oggetti java senza l'uso delle scriplet.

Il tag più utilizzato è il tag c:out che sostituisce il codice scriplet <%= … >, con la grandissima differenza che nelle scriplet bisogna utilizzare le istruzioni java sugli oggetti mentre con il c:out ciò non è necessario, per esempio

 <%= oggetto.getProprieta().getSottoProprieta()%>

diventa in JSTL:

<c:out value="${oggetto.proprieta.sottoProprieta}" />

e, dalle ultime versioni di JSTL, è possibile usare il comando con il carattere $ senza l'uso del tag C:OUT quindi l'esempio diventa:

 ${oggetto.proprieta.sottoProprieta}

Cosa molto importante è che, nelle scriplet bisogna sempre ragionare in termini di request, sessione e page mentre JSTL unisce tutti gli "scope", cercando un oggetto prima in page, poi in request e poi in sessione. Per esempio:

 <%=((oggetto) session.getAttribute("oggetto")).getProprieta()) %>

diventa in JSTL :

 ${oggetto.proprieta}

rendendo il codice molto più snello e leggero da leggere. Con il tag c:set è possible valorizzare una variabile assegnandogli una varibile, per esempio

<c:set var = "salario" scope = "session" value = "1500"/>
<c:set var = "salarioAnnuale" scope = "request" value = "${salario*13}"/>

in questi esempi è possibile vedere come il tag ha bisogno del nome della varibile di destinazione, lo "scope" che può essere request, session o page, e il valore. Questo tag prevede anche il passaggio del valore all'interno del tag XML, per esempio:

<c:set var = "salario" scope = "session">1500

però questo metodo è sconsigliato. E' possibile anche usare il c:set per valorizzare proprietà di un oggetto, per esempio

<c:set target = "oggetto" property="proprieta" scope = "session" value="valore" />

Da notare che con c:set non si specifica la classe dell'oggetto che si va a valorizzare, la libreria infatti effettua dei cast automatici per la valorizzazione delle variabili.

Nella libreria è presente anche il tag c:remove utilizzato per eliminare oggetti dalla sessione (valorizzando l'oggetto con il valore NULL). Per esempio:

<c:remove var = "oggetto" scope="session" />

In questa libreria gli scope possibili sono: page, request e session ma è stato aggiunto un ulteriore scopo "param" utilizzato per recupeare i valori passati via GET o via POST per esempio dai form, questo è molto utile, visto che è possibile recuperare i parametri che arrivano alla pagina con l'istruzione

${param.nomeParametro}

Quando si usa l'istruzione ${oggetto.proprieta} di default viene usato il context della pagina (pageContext) ma se non trovato viene cercato in request, session e application a cascata. Se si vuole utilizzare una variabile in uno scope specifico si può usare l'iscrizione ${scopeSpecifico.oggetto.proprieta}, dove gli scope disponibili sono:

  • pageContext : rappresenta la pagina generale
  • pageScope : usato per accedere alla variabili dichiarate in pagina
  • requestScope : usato per accedere allo scope Request
  • sessionScope : usato per accedere allo scope Session
  • applicationScope : usato per accedere allo scope Applicazion
  • param : elenco dei parametri della requst, ogni parametro ha il suo nome
  • paramValues : elenco dei parametri della request, in formato array di valori
  • header : elenco dei valori presenti nell'header
  • headerValues : elenco dei valori presenti nell'header, in formato array di valori
  • cookie : elenco dei valori presenti nei cookie

La libreria JSTL mette a disposizione un paio di tag molto utili per le importazioni e la navigazione, questi tag voglio andare a sostituire import e redirect tra jsp tramite il tag nativo

 <c:import url = "NomeAction.do" var="nomeVariabile"/>

Il tag redirect esegue proprio il redirect verso una URL specifica, per esempio

 <c:redirect url = "https://www.alnao.it"/>

I tag Url e Param, se combinati, permettono di definire una variabile (lato server), con un url e i parametri, per esempio:

 <c:url value = "/NomeAction.do" var = "myURL">
<c:param name = "parametro1" value = "valoreUno"/>
<c:param name = "parametro2" value = "valoreDue"/>
</c:url>
<c:import url = "${myURL}"/>

il risultato myURL sarebbe NomeAction.do?parametro1=valoreUno&parametro2=valoreDue. Da notare che le variabile create con questi tag possono essere usate anche nei tag A di HTML, per esempio

<a href="${myURL}">Link a NomeAction</a>

La libreria mette a disposizione tre tag fondamentali per la costruzioni di pagine complesse con condizioni e iterazioni, la condizione più semplice è il tag < c:if> che valuta un'espressione e esegue il codice contenuto all'interno del tag se la condizione è verificata come vera

 <c:if test = "${importo > 2000}">
<p>L'importo di < c:out value = "${salary}"/> supera i 2000</p>
</c:if>

nella proprietà test del tag si possono valutare qualsiasi tipo di condizione però bisogna ricordarsi di prestare attenzione a non inserire spazi non desideati, perchè purtroppo

 <c:if test="true ">…</c:if>

risulta sempre vuota a causa dello spazio posizionato all'interno dei doppi-appici, ovviamente però gli spazi all'interno delle parentesi graffe sono permesse. L'altro tag per le condizion è è il choose che funziona come il comando switch in java, questo tag necessita di un elenco di when contenente le condizioni di test e un otherwise corrispondente al default java.

<c:choose>
<c:when test = "${importo <= 0}">
Importo negativo
</c:when>
<c:when test = "${importo > 1000}">
Importo molto alto
</c:when>
<c:otherwise>
Importo medio
</c:otherwise>
</c:choose>

Il tag per eseguire i cicli è il forEach che prevede alcune varianti a seconda delle esigenze: il classico ciclo for da un numero ad un altro (estremi compresi) si realizza nella forma

 <c:forEach var = "i" begin = "1" end = "5">
<p>Item < c:out value = "${i}"/></p>
</c:forEach>

Voler invece ciclare su una lista costante di elementi si usa:

 <c:forTokens items = "Zara,nuha,roshy" delims = "," var = "name">
<p>< c:out value = "${name}"/></p>
</c:forTokens>

Se invece si vuole iterare una lista di elementi non costante si usa:

 <c:forEach items="${variabileElenco}" var="elmento" varStatus="status">
<p>Item posizione ${status.index} con valore ${elemento} </p>
</c:forEach>

La potenzialità di questi tag è che non è prevista l'indicazione del tipo, in particolare il tag forEach visto nel terzo caso funziona sia con array semplici, con tipi List, con mappe Map, con iteratori Iterator, e qualsiasi tipo di Collection java. Un'altra caratteristica degli oggetti lista (array o Collection) è l'utilizzo della condizione empty per vedere se la lista contiene elementi, per esempio:

 <c:if test="${empty variabileElenco }">
<p>La lista vuota</p>
</c:if>

Un tag particolare è stato introdotto per la gestione degli errori runtime e le exception, è infatti possibile catturare gli errori in fase di esecuzione della pagina, un esempio semplice:

 <c:catch var ="catchException">
<% int x = 5/0;%>
</c:catch>
<c:if test = "${catchException != null}">
<p>L'exception lanciata è : ${catchException}
Con il messaggio: ${catchException.message}</p>
</c:if>

Nelle condizioni è possibile usare le condizioni java pure (==) oppure quelle descrittive (eq), un elenco delle possibile condizioni

  • uguaglianza == eq
  • diverso != ne
  • minore < lt
  • maggiore > gt
  • maggiore o uguale >= ge
  • minore o uguale <= le
  • condizione and && and
  • condizione or || or
  • negazione ! not
  • vuoto XX empty
  • addizione +
  • sottrazione -
  • moldiplicazione *
  • divisione / div
  • modulo % mod

da notare che empty funziona con i valori null, con le liste senza nessun elemento e con le stringhe senza caratteri ma non con i numeri (zero non è empty).

La libreria mette a disposizione anche un gruppo di tag per la formattazione, cioè usati per visualizzare in pagina dati che nel server sono in formato particolare, il prefisso da usare è il fmt che si importa con l'istruzione

 <%@ taglib prefix = "fmt" uri = "http://java.sun.com/jsp/jstl/fmt" %>

Il tag più usati sono quelli per formattare i numeri e , il primo è usato per mostrare a pagina numeri, alcuni esempi dell'uso delle varie opzioni:

<c:set var = "balance" value = "120000.2309" />
<p>senza parametri: < fmt:formatNumber value = "${balance}" type = "currency"/>< /p>
<p>con il massimo di interi : < fmt:formatNumber type = "number" maxIntegerDigits = "3" value = "${balance}" />< /p>
<p>con il massimo di decimali: < fmt:formatNumber type = "number" maxFractionDigits = "3" value = "${balance}" />< /p>
<p>visualizzato come percentuale: < fmt:formatNumber type = "percent" maxIntegerDigits="3" value = "${balance}" />< /p>
<p>percentuale con minimo di decimali: < fmt:formatNumber type = "percent" minFractionDigits = "10" value = "${balance}" />< /p>
<p>percentuale con il massimo di interi: < fmt:formatNumber type = "percent" maxIntegerDigits = "3" value = "${balance}" />< /p>
<p>esadecimale: < fmt:formatNumber type = "number" pattern = "###.###E0" value = "${balance}" />< /p>
<p>Con un locale particolare, per esempio in usa si usa il punto come separtore dei decimali mentre in Italia è la virgola
< fmt:setLocale value = "en_US"/>
< fmt:formatNumber value = "${balance}" type = "currency"/>
</p>

Mentre il tag parseNumber serve a eseguire un "cast" tra i tipi, per esempio:

<fmt:parseNumber var = "variabile" type = "number" value = "${balance}" />
<p>Parse senza parametri: < /p>
<fmt:parseNumber var = "variabile" integerOnly = "true" type = "number" value = "${balance}" />
<p>Parse solo come integer: < /p>

Altra coppia di tag a disposizione è quella per la formattazione delle date

<fmt:formatDate> e < fmt:parseDate>, esempi di utilizzo:
<c:set var = "now" value = "20-10-2010" />
<fmt:parseDate value = "${now}" var = "parsedEmpDate" pattern = "dd-MM-yyyy" />
<p>Parsed Date: < /p>
<c:set var = "now" value = "<% = new java.util.Date()%>" />
<p>Visualizza l'ora: < fmt:formatDate type = "time" value = "${now}" />< /p>
<p>Visualizza la data (il mese in lettere): < fmt:formatDate type = "date" value = "${now}" />< /p>
<p>Mostra data e ora (il mese in lettere): < fmt:formatDate type = "both" value = "${now}" />< /p>
<p>Mostra data e ora (il mese in numerI): < fmt:formatDate type = "both" dateStyle = "short" timeStyle = "short" value = "${now}" />< /p>
<p>Mostra data e ora (il mese con il nome intero) < fmt:formatDate type = "both" dateStyle = "long" timeStyle = "long" value = "${now}" />< /p>
<p>MOstra la data in formato personalizzato con un pattern specifico: <fmt:formatDate pattern = "yyyy-MM-dd" value = "${now}" />< /p>

Altra coppia disponibile è il tag per la visualizzazione dei messaggi localizzati come visto nell'articolo dedicato è possibile creare file diversi per lingua ed è disponibile

<fmt:setLocale value = "en"/> <!--en/it/de/... -->
<fmt:setBundle basename = "NomeBundle" var = "lang"/>
<fmt:message key = "NomeLabel1" bundle = "${lang}"/>
<fmt:message key = "NomeLabel2" bundle = "${lang}" var="variabileLabel2"/>
<c:out value = "${variabileLabel2}" />

La libreria JSTL mette a disposizione un insieme di funzioni specifiche, con lo scopo di evitare che il programmatore debba inserire scriplet all'interno del codice della jsp, queste funzioni sono disponibili con il prefisso fn:

<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

Le funzioni disponibili sono

  • fn:contains testa se una stringa è contenuta in un'altra, per esempio
<c:set var = "theString" value = "I am a test String"/>
<c:if test = "${fn:contains(theString, 'test')}">
Found test string< /c:if>
  • fn:containsIgnoreCase() come la precedente ma ignora maiuscole e minuscole
  • fn:endsWith testa se una stringa termina con una determinata stringa
<c:set var = "theString" value = "I am a test String 123"/>
<c:if test = "${fn:endsWith(theString, '123')}">String ends with 123< /c:if>
  • fn:escapeXml() codifica gli elementi XML all'interno di una stringa come testo e non come tag HTML
<c:set var = "string2" value = "This is second String."/>
<p>string: ${fn:escapeXml(string2)}< /p>
visualizzato: This is second String.
  • fn:indexOf() calcola la posizione di una stringa all'interno di un altra
  • fn:join() unisce tutti gli elementi di un elenco in una stringa separati da un separatore
 <c:set var = "string1" value = "This is first String."/>
<c:set var = "string2" value = "${fn:split(string1, ' ')}" />
<c:set var = "string3" value = "${fn:join(string2, '-')}" />
<p>${string3}< /p>
  • fn:length() calcola il numero di elementi in una lista (collection) oppure la lunghezza di una stringa
  • fn:replace() ritorna una stringa nella quale è stato sostituita tutte le occorrenze di una stringa con un'altra
  • fn:split() divide una stringa in un array di sottostringhe
  • fn:startsWith() testa se una stringa inizia con una determinata stringa
  • fn:substring() ritorna una sottostringa da una posizione ad un'altra (si comincia a contare da zero come in java), per esempio
<c:set var = "string1" value = "This is first String." />
<c:set var = "string2" value = "${fn:substring(string1, 5, 15)}" />
<p>is first S< /p>
  • fn:toLowerCase() trasforma tutti i caratteri di una stringa in minuscolo
  • fn:toUpperCase() trasforma tutti i caratteri di una stringa in maiuscolo
  • fn:trim() rimuove tutti gli spazi bianchi all'inizio e alla fine di una stringa

Sfruttando la libreria JSTL, è possibile definire funzioni che poi sarà possibile utilizzare com il formato

${prefisso:funzione('parametro1','parametro2')}

prima di tutto bisogna definire una classe e un metodo che verrà eseguito

package it.alnao.javaee.tags;
import javax.servlet.jsp.tagext.TagSupport;

public class Functions extends TagSupport {
 public static Integer string2int(String val){
    return MyNumberUtils.parseInteger(val);
 }
 public static String unisciConSepartore(String s1,String s2){
    return s1+"-"+s2;
 }
}

Poi bisogna definire un file TLD nel web.xml e scrivere il file xml con la definizione dei metodi:

<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
<description>Prova tag library
<tlib-version>1.0
<short-name>provaTag
<function>
<name>string2int
<function-class>it.alnao.javaee.tags.Functions
<function-signature>java.lang.Integer string2int(java.lang.String)
</function>
<function>
<name>unisciConSepartore< /name>
<function-class>it.alnao.javaee.tags.Functions< /function-class>
<function-signature>java.lang.String unisciConSepartore(java.lang.String,java.lang.String)< /function-signature>
</function>
</taglib>

In pagina poi basterà importare la libreria con

 <%@ taglib uri="/WEB-INF/tld/provaTag.tld"                                prefix="provaTag" %>

e poi usare le funzioni create

 ${provaTag:string2int('123')}
${provaTag:unisciConSepartore('prova','unione')}

La potenzialità di questo metodo è che viene definito del codice java riutilizzabile in più pagine.

Con Eclipse è possibile eseguire debug velocemente su qualsiasi classe java in esecuzione nel webserver, comprese anche quelle utilizzate all'interno delle applicazioni JavaEE, lo scopo principale del debug è trovare errori rilevati nel software oppure comprendere il funzionamento del codice stesso.

Se il server è agganciato ad eclipse nella vista "Servers" compare una opzione "Debug" con un'icona simile ad una coccinella, premendo questa opzione il server si avvierà (o riavvierà) in modalità debug, modalità indispensabile per poi poter fermare l'esecuzione nei punti desiderati.

Se invece il server non è agganciato ad Ecplipse ma si trova sullo stesso computer in cui è in esecuzione Eclipse è possibile configurarlo dal menù "Run" alla voce "Debug configuration" e selezionando "Remote debug application", impostando il nome, l'indirizzo, la porta e soprattutto quali sono i sorgenti corrispondenti all'applicazione in debug.

Una volta partita la modalità debug è possibile aprire la vista Debug dal menù "Window" alla voce "Open prospective", da questa prospettiva abbiamo a disposizione simile a quella Java ma con alcune viste specifiche:

  • breakpoints dove sono indicati tutti i punti di interruzione con le varie opzioni per la disattivazione
  • variabiles dove sono indicate tutte le variabili e degli oggetti esistenti in cui il debugger è fermo con il valore di ogni elemento
  • expression dove sono indicate le espressioni che l'utente ha inserito e il risultato dell'istruzione
    Da notare che nella barra delle icone è comparso un nuovo menù dedicato con alcune icone
  • Resume (icona play verde): esegue il punto di debug fino al prossimo punto di interruzione
  • Suspend (icona stop gialla): interrompe temporaneamente la modalità debug
  • Terminate (icona stop rossa): termina il punto di debug, fermando l'esecuzione
  • Disconnect (icona rete rossa): disconnette il debugger di eclipse e il server tornerà in modalità normale
  • Step into (icona freccia quadrata) : esegue le sottoistruzioni del punto in cui è fermo il debug
  • Step over (icona freccia torna) : esegue l'istruzione e il punto di debug si ferma alla prossima istruzione
  • Step return (icona freccia verso l'alto) : esegue l'istruzione e tutte le successive finchè il debugger non trova un return

Per aggiungere un punto di interruzione (breckpoint) basta andare in qualsiasi classe java, premere sul numero di riga desiderato e selezionare l'opzione "toogle breakpoint", questo attiva o disattiva il punto che compare anche nella vista dedicata. Se il server è avviato correttamente in modalità debug e qualcuno esegue quella classe, l'interruzione verrà fermata (non terminata) e il debugger si attiverà portando l'utente di eclipse al punto desiderato e attivando i bottoni elencati poco sopra.

Da notare che questa tecnica di debug è possibile su qualsiasi classe java compilata ma non sulle jsp che, non essendo compilate in file class, non possono essere debuggate.

Apache Ant è un software per rendere automatici i processi di compilazione e rilascio, è possibile usarlo per la creazione di file jar, di file war ma è possibile usarlo per manipolare i file system ed eseguire semplici operazioni. Il vantaggio di usare i file ant è che non dipendono dalla piattaforma e funzionano con tutti sistemi operativi infatti definiscono un tag specifico per ogni comando.
I file Ant sono un file xml dove sono descritti i passi da eseguire,

<?xml version="1.0"?>
<project name="Hello" default="init">
	<property name="jar.dir" value="build"/>
	<property name="jar.name" value="${jar.dir}/fileCreato.jar"/>
    <target name="clean" description="Cancella tutti i file dall'interno di una cartella">
        <delete dir="classes"/>
		<delete dir="build"/>
    </target>
    <target name="compile" description="Compila tutti i file java in file class">
        <mkdir dir="classes"/>
        <javac srcdir="." destdir="classes"/>
    </target>
    <target name="creaJar" depends="compile" description="Crea un jar">
        <jar destfile="${jar.name}">
            <fileset dir="classes" includes="**/*.class"/>
            <manifest>
                <attribute name="Main-Class" value="Prova"/>
            </manifest>
        </jar>
    </target>
	<target name="deploy" depends="creaJar" description="Copia il file jar in una cartella">
		<copy todir="C:/server/cartella/">
			<fileset dir="build/fileCreato.jar"/>
		</copy>
	</target>
	<target name="init" description="Script principale che poi richiama il deploy">
		<tstamp>
			<format property="init.timestamp" pattern="dd/MM/yyyy HH:mm:ss"/>
		</tstamp>
		<echo message="Inizio il processo ${init.timestamp}"/>
		<antcall target="clean"/>
		<antcall target="deploy"/>
		<antcall target="clean"/>
		<echo message="Fine processo"/>
	</target>
</project>

Nell'esempio riportato abbiamo definito un file xml con un root-tag project: è indicata una proprietà default dove è descritto lo step da eseguire di default.

  • Il primo target è clean che, oltre ad avere una descrizione, esegue la cancellazione di due cartelle, questo step è usato per pulizia.
  • Il secondo target è compile che, dopo aver creato la cartella clases, esegue la chiamata al compilatore javac per creare i file class dai file java del progetto.
  • Il terzo target è la creazione del jar cioè viene eseguito il comando jar, con un elenco di classi indicato nel fileset e un manifest specifico per la main class del jar
  • Il quarto target esegue la copia del file jar in una cartella del file system dell'utente, cioè dal workspace ad un'altra cartella
  • Il quinto target è quello indicato di default e esegue visualizza un messaggio nella console, esegue la pulizia, il deploy e poi pulisce tutto di nuovo.

Da notare che il quarto ha come dipendenza il terzo, cioè ogni volta che viene eseguito il quarto (deploy), prima viene eseguito la terzo (creaJar) così come il terzo dipende dal secondo, in questo modo è possibile creare file xml snelli e semplici.

All'interno del file Ant è possibile anche definire delle proprietà come fatto per il jar.name, valori poi utilizzabili nei vari target senza dover ripetere i valori, come fossero delle costanti. E' possibile anche creare dei file di properties ed importarli con il comando

<property file="./build.properties"/>

Inoltre è possibile indicare un classpath particolare al compilatore per esempio con una properties

weblogic.home=C:/server/cartellaJar/
lib.path=../../cartella/file.jar;${weblogic.home}nomeFile.jar;

e il suo tag nel file Ant

<path id="javac.classpath">
<pathelement path="${lib.path}"/>
</path>

Su eclipse per eseguire il file ant basta selezionare il file e l'opzione "Run as Ant Build" mentre da riga di comando è disponibile il comando

ant –buildfile NomeFile.xml

All'interno delle applicazioni JEE è possibile creare connessioni tra il server Web e un Server FTP utilizzando il protocollo omonimo o il protocollo sicuro SFTP. Per usare i protoccoli useremo le librerie base di Apache disponibili nel jar commons della libreria quindi basta imporare queste classi

Per il protocollo FTP servono questi import

     import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.io.output.ByteArrayOutputStream;

Per il protocollo SFTP servono questi import

     import org.apache.commons.vfs2.impl.;     
import org.apache.commons.vfs2.provider.sftp.;
import org.apache.commons.vfs2.*;

Un semplice esempio usando il protocollo FTP per scaricare un file e/o una immagine:

public String getStringImageFromFTP(String serverAddress, String userId, String password, int port, String urlFile) throws Exception{   
    FTPClient ftpClient = new FTPClient();
    ftpClient.connect (serverAddress, port);
    ftpClient.login (userId, password);
    ftpClient.enterLocalPassiveMode ();
    ftpClient.setFileType (FTP.IMAGE_FILE_TYPE);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    boolean success = ftpClient.retrieveFile(urlFile, baos);
    ftpClient.disconnect();
    if (success) {
        String s=baos.toByteArray();
        //questo esempio trasforma il file arrivato in una immagine base64
        s="data:image/bmp;base64," + DatatypeConverter.printBase64Binary(baos.toByteArray()); 
        //nota: necessario import javax.xml.bind.DatatypeConverter;
        return s;
    }else{
        //gestione errore
    }
}

Un semplice esempio usando il protocollo SFTP per scaricare un file o una immagine:

public String getStringImageFromFTP(String serverAddress, String userId, String password, int port, String urlFile) throws Exception{
    FileObject destn;
    StandardFileSystemManager manager = new StandardFileSystemManager();
    manager.init();//Initializes the file manager
    //Setup our SFTP configuration
    FileSystemOptions opts = new FileSystemOptions();
    SftpFileSystemConfigBuilder.getInstance().setStrictHostKeyChecking(opts, "no");
    SftpFileSystemConfigBuilder.getInstance().setUserDirIsRoot(opts, true);
    SftpFileSystemConfigBuilder.getInstance().setTimeout(opts, 10000);

    //Create the SFTP URI using the host name, userid, password,  remote path and file name
    String sftpUri = "sftp://"+userId+":"+password+"@"+serverAddress+":"+port+"/"+urlFile;
    // Create remote file object
    FileObject remoteFile = manager.resolveFile(sftpUri, opts);

    //trasformo il FileObject remoto in un array di byte per poi la trasformazione in String
    org.apache.commons.vfs2.FileContent fc = remoteFile.getContent();
    int bytesRead = 0;
    java.io.InputStream is = fc.getInputStream();
    byte[] b = new byte[1024];
    int fcs = new java.math.BigDecimal(fc.getSize()).intValueExact();
    java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(fcs);
    while ((bytesRead = is.read(b,0,b.length)) != -1) { buf.put(b,0,bytesRead); }
    remoteFile.close();
    org.apache.commons.vfs2.FileSystem fs = remoteFile.getFileSystem();
    manager.closeFileSystem(fs);
    byte[] bytes = buf.array();
    return  "data:image/bmp;base64," + DatatypeConverter.printBase64Binary(bytes);
    //necessario import javax.xml.bind.DatatypeConverter;
}

Esistono varie librerie per l'esposizione di servizi, la libreria più vecchia ma ancora molto usata è Axis che utilizza il protocollo Corba per la comunicazione e il formato XML la rappresentazione dei dati.
Come con qualsiasi altra libreria, l'importazione di Axis all'interno del progetto prevede solo la copia delle librerie jar all'interno della cartella lib e la configurazione del classPath in Eclipse per lo svilippo, in particolare è necessario importare le librerie:

  • axis.jar
  • commons-discovery-0.2.jar
  • commons-logging.jar
  • jaxrpc.jar
  • saaj.jar
  • wsdl4j.jar

Mentre nel web.xml è necessario aggiungere le servlet di ApacheAxis

  <servlet>
    <display-name>Apache-Axis Servlet</display-name>
    <servlet-name>AxisServlet</servlet-name>
    <servlet-class>org.apache.axis.transport.http.AxisServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>AxisServlet</servlet-name>
    <url-pattern>/servlet/AxisServlet</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>AxisServlet</servlet-name>
    <url-pattern>*.jws</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>AxisServlet</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
  <servlet>
    <display-name>Axis Admin Servlet</display-name>
    <servlet-name>AdminServlet</servlet-name>
    <servlet-class>org.apache.axis.transport.http.AdminServlet</servlet-class>
    <load-on-startup>100</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>AdminServlet</servlet-name>
    <url-pattern>/servlet/AdminServlet</url-pattern>
  </servlet-mapping>

Poi all'interno del progetto basta creare una classe ed i metodi del webservice

package it.alnao.jee;
public class ProvaWebService {
	public String echo(String message) {	
		System.out.println("Messaggio "+ message);
		return "Argentina WebService message:" + message;
	}
	public ValoreOutput calcolaDoppi ( ValoreInput input ) throws Exception{
		ValoreOutput output=new ValoreOutput();
		output.setValore(input.getValore() * 2 );
		return output;
	}
}

nel primo metodo input e output sono semplici stringhe mentre nel secondo esempio input e output sono due oggetti di classi complesse.

Per generare i componenti del webservice si può usare la procedura guidata di eclipse che prevede di creazione un progetto di appoggio dove verranno generate le classi per il servizio, per l'esecuzione di questa procedura guidata, nel sistema locale bisogna avere un webserver come Tomcat.

La procedura guidata inizia selezionando la classe con i metodi del servizio, poi nel menà principale selezionare la voce "New" (nuovo), al suo interno la voce "Web Service", a questo punto dovrebbe comparire la videata del primo passo della procedura.

In questo primo passo possiamo selezionare la classe del servizio che dovrebbe essere già valorizzata perché selezionata in precedenza e compaiono due blocchi: uno per il server e uno per il client, a questo punto conviene selezionare in entrambi i blocchi i valori più alti, controllando che nel primo blocco sia selezionato Apache Axis come libreria e come server il Tomcat precedentemente configurato. Nel secondo blocco, dedicato ad un client di prova, controlliamo le stesse configurazioni. Inoltre in questo passo è possibile definire i nomi dei due progetti di appoggio che vengono creati: uno per il server e uno per il client.

Nella seconda videata è necessario definire il nome e la posizione del WSDL e l'elenco dei metodi da importare nel servizio mentre nei passi successivi ci sono alcuni step per la creazione e l'esecuzione immediata del client di prova generato da eclipse.

Se è stato generato il client, automaticamente Eclipse apre un mini-browser dove viene caricata la pagina del client con il quale è già possibile provare il WebService e i metodi implementati nel server, da tenere conto che viene tutto eseguito sul Tomcat configurato. In questo mini-client vengono creati anche due metodi supplemtnare getEndPoint e setEndPoint che servono per leggere e impostare l'url del webservice, se l'utente prova a leggere l'url ed inserirlo in un browser normale aggiungendo "?wsdl" alla fine si ottiene il WSDL.

In automatico nel progetto del server è stato generato un file con estensione WSDD (WebService) e un file con estensione WSDL (Web Services Description Language), il primo serve al server mentre il secondo serve al client. Il file WSDD deve essere deployato assieme alle librerie e alla classe nel webserver, in particolare il file WSDD deve essere posizionato nella stessa posizione del web.xml e viene caricato all'avvio dell'applicazione JEE dalle librerie Axis definite nel Web.xml, ogni volta che si modifica la classe server deve essere rigenerato il file WSDD solo se si aggiungono metodi o si modificano le classi di input e/o di output. Il file WSDL non deve essere deployato e può essere rigenerato accedendo al webserver all'indirizzo

http://indirizzo:porta/NomeApplicazione/services/NomeClasse?wsdl

tale file viene usato per la creazione delle classi per il client.

Se non si dispone di un client è possibile provare il server creato con programmi come SOAP-UI grazie al fatto che il WSDD e il WSDL generato sono XML standard e possono essere chiamati da server di tutti i tipi.

Per la creazione di un Client per un servizio WebService è possibile usare la procedura guidata di Eclipse e sempre le librerie Axis, infatti Eclipse ha proprio un tipo di progetto "Web Service Client" creabile tramite procedura guidata.

La procedura guidata del client ha solo bisogno del WSDL e nel progetto creato vengono generate pagine jsp e alcune classi java, le pagine jsp sono delle semplici pagine per il test del client nel browser di eclipse.

Le classi java genrate invece sono molto imporanti perchè sono generati tutte le classi di input e di output come bean pronti per l'uso e sono generate delle classi Proxy per il servizio, tutte queste classi non devono essere mai separate e devono essere mantenute nello stesso package, al massimo è possibile canbiare package a tutte le classi se c'è la necessità di importarle all'interno di un progetto.

Per la chiamata al WebService da una pagina jsp o all'interno di un metodo java basta scrivere il codice:

int valore=2;
String endPoint = "http://indirizzo:porta/NomeApplicazione/services/NomeClasse"; //senza ?WSDL
ProvaWebServiceProxy webServiceProxy = new ProvaWebServiceProxy( endPoint );
ValoreInput input = new ValoreInput;
ValoreOutput output;
input.setValore( valore );
output = webServiceProxy.calcolaDoppi( input ); //chiamata al webService
System.out.println( "Valore=" + output.getValore() );

In questo semplice esempio viene utilizzata la classe proxy dove è indicato l'endPoint del server che espone il web service, un oggetto per l'Input e un oggetto per l'Output.

Ovviamente l'utilizzo di questa tecnologia e di queste librerie è indicata per semplici progetti, esistono librerie più complesse utilizzabili per progetti di grandi dimensioni o esigenze maggiori come i temi di autenticazione e di sicurezza.

L'espressione "representational state transfer" e il suo acronimo "REST" sono stati introdotti agli inizi del secolo da Roy Fielding, uno dei principali creatori dell'HTTP, che descriveva REST come uno stile architetturale, cioè un'idea di architettura per sistemi hypermedia distribuiti. REST infatti non definisce i dettagli dell'implementazione delle componenti client/server e nemmeno la sintassi del protocollo ma definisce i ruoli dei vari componenti: vincoli sulla loro interazione e la loro interpretazione.
I principi di RESTful sono:

  • architettura client/server: il noto paradigma dell'informatica moderna "separation of concerns (SoC)" cioè la separazione dei compiti. SoC è il principio di progettazione per separare un sistema in moduli distinti, in modo tale che ogni modulo si preoccupi di un certo compito.
  • stateless (come con HTTP, ma non necessariamente): la comunicazione tra client e server deve essere senza stato tra le richieste. Ciò significa che ogni richiesta da parte di un client dovrebbe contenere tutte le informazioni necessarie per il servizio per comprendere il significato della richiesta. In breve ogni richiesta è come se fosse la prima richiesta e non è correlata ad una precedente richiesta.
  • a livelli: in un Architettura REST i messaggi di risposta dal servizio ai suoi consumatori sono esplicitamente etichettati come cachabili o non. In questo modo, è possibile memorizzare nella cache la risposta per il riutilizzo nelle richieste successive.
  • interfaccia uniforme: per avere un caching efficiente in una rete, i componenti devono essere in grado di comunicare tramite un’interfaccia uniforme.
  • rappresentazione dei dati uniforme: una risosra può essere rappresentata in molti modi diversi (JSON, XML, HTML, JPEG, ecc..). La rappresentazione più famosa utilizzata nelle implementazioni REST è il JSON

La separazione totale tra client e server: il protocollo REST separa completamente l'interfaccia utente dal server. Questo ha alcuni vantaggi in fase di sviluppo:

  • permette in grossi progetti di separare nel modo migliore il lavoro tra developer;
  • evoluzione indipendente delle diverse componenti degli sviluppi: è possibile avere più client scritti in linguaggi diversi (Struts, Spring, Angular) con lo stesso server
  • portabilità dell'interfaccia ad altri tipi di piattaforme: REST può essere usato anche per sviluppare un applicazione Mobile senza costi ulteriori
  • visibilità, affidabilità e scalabilità: client e Server possono risiedere su server differenti e come già spiegato è facilissimo scalare orizzontalmente un componente dell’architettura con pochissima difficoltà e con nessun impatto per l’altro componente.
  • indipendenza da linguaggi e tecnologie specifiche

Il protocollo HTTP fornisce diversi metodi per implementare il prinicipio CRUD (Creae Read Update Delete); ogni diversa funzionalità che voglio fornire per permettere l'interazione con le risorse del mio dominio deve essere mappata con il giusto metodo HTTP:

  • POST per creare
  • GET per leggere
  • PUT Per aggiornare
  • DELETE per eliminare

Alcuni esempi di HTTP

PUT /v1/people/1 HTTP/1.1 
Host: https://api.myapi.it
Content-Type: application/json
{ "firstname": "Mario", ... }

POST /v1/people HTTP/1.1
Host: https://api.myapi.it
Content-Type: application/json
{"firstname": "Dario", "lastname": "Frongi", "age":30 }

GET /v1/people HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
[{"id":1,"firstname": "Dario","lastname": "Frongi","age":30},
{"id":2,"firstname": "Mario","lastname": "Rossi","age":30}]

Dove i codici HTML delle risposte significano:

  • 200 Ok: La risposta standard per rappresentare una operazione di tipo GET, PUT or POST (per azioni e non creazione risorsa) avvenuta con successo.
  • 201 Created: Questo codice andrebbe utilizzato lato server per comunicare che la nuova istanza è stata creata con successo. Ad esempio nella creazione di una nuova risorsa usando il metodo POST.
  • 204 No Content: significa che la richiesta è stata correttamente processata, ma non ha ritornato nessun contenuto. DELETE è un ottimo esempio di utilizzo di questo codice.
  • 304 Not Modified: indica che il client ha già in cache la risposta e non è necessario reinviarla.
  • 400 Bad Request: indica che la richiesta non è stata processata perché il server non riconosce la richiesta come una tra quelle attese: ad esempio url errata o nome parametro errato.
  • 401 Unauthorized: indica che il client non ha i privilegi per accedere a tale risorsa (la API richiesta richiede l’autenticazione o un token di autenticazione).
  • 403 Forbidden: indica che la richiesta effettuata è valida e il client è correttamente autenticato, ma non ha i privilegi per poter utilizzare questa API
  • 404 Not Found: indica che la risorsa richiesta non esiste o è momentaneamente non recuperabile.
  • 410 Gone: indica che la risorsa richiesta non è disponibile e intenzionalmente spostata.
  • 500 Internal Server Error: indica che la richiesta è valida, ma il server ha riscontrato un errore non gestito (ad es. in java una eccezione non correttamente catturata).
  • 503 Service Unavailable: indica che il server è offline e non può quindi processare richieste.

La libreria Jersey è una delle più usate nelle vecchie applicazioni JEE dove è già presente un altro framework, per esempio questa libreria è compatibile con le applicazioni con Struts, infatti questa libreria aggiunge una servlet che fungerà da end-point per il nostro sistema REST.  La libreria utilizza lo standard JAX-RS ed è scaricabile dal sito ufficiale (ad ora la versione consigliata è la JAX-RS 2.1 e Jersey 2.29.1), da questo sito si possono scaricare le librerie con maven o a mano con uno zip per poi posizionarle nella cartella lib del progetto JEE, in questo secondo caso bisogna ricordare che tutti i file jar presenti nello zip sono necessari. Dopo aver installato le librerie bisogna censire la servlet nel file web.xml del progetto

<servlet> 
<servlet-name>Jersey REST Service</servlet-name>
<servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>it.alnao.example.rest.ServizioRestClass</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Jersey REST Service</servlet-name>
<url-pattern>/ServiziRest/*</url-pattern>
</servlet-mapping>

Per poi creare una classe all'interno del package indicato nel web.xml nel tag "param-value" della servlet, la classe deve avere un path e, per ogni metodo, bisogna definire il tipo (GET/POST), il path e i tipi di input e di output che saranno di tipo JSON.

package it.alnao.example.rest;

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import java.io.*;
import java.util.*;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.*;

@Path("/ServizioRest")
public class ServizioRestClass extends javax.ws.rs.core.Application{
@GET // http://localhost:porta/ServiziRest/ServizioRest/echo

@Path("/echo")
public String echo(){
return "Ciao";
}

@POST // http://localhost:porta/ServiziRest/ServizioRest/echoMessaggio
@Path("/echoMessaggio/{message}")
public String echoMessaggio(@PathParam("message") String message){
return "ECHO: " + message;
}

@POST
@Path("/Elenco") // http://localhost:porta/ServiziRest/ServizioRest/Elenco
@Consumes(MediaType.APPLICATION_JSON) //Request
@Produces(MediaType.APPLICATION_JSON) //Response
public Response getElenco(MyRestRequest request){
MyRestResponse esito=new MyRestResponse();
try {
ArrayList<String> list=new ArrayList<String>();
list.add(request.getNome());
esito.setElenco(list);
esito.setEsito("OK");
}catch(Exception e){
e.printStackTrace();
esito.setEsito("KO");
esito.setDescErrore(e.getMessage());
return Response.status(500).entity(esito).build();
}
return Response.status(200).entity(esito).build();
}
}

E bisogna ricordarsi di definire i bean di request e response con la direttiva XmlRootElement, eventuali oggetti sottoclasse devono avere la stessa direttiva.

package it.alnao.example.rest;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class MyRestRequest {
private String nome;

public MyRestRequest(String nome) {
super();
this.nome = nome;
}
public MyRestRequest(){}

@Override
public String toString() {
return "MainPageRequest [nome=" + nome + "]";
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
}

La stessa direttiva per il bean di response

package it.alnao.example.rest;

import java.util.ArrayList;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class MyRestResponse {
private String esito;
private String descErrore;
private ArrayList<String> elenco;

@Override
public String toString() {
return "MyRestResponse";
}

public MyRestResponse(){}

public String getEsito() {
return esito;
}
public void setEsito(String esito) {
this.esito = esito;
}
public String getDescErrore() {
return descErrore;
}
public void setDescErrore(String descErrore) {
this.descErrore = descErrore;
}
public ArrayList<String> getElenco() {
return elenco;
}
public void setElenco(ArrayList<String> elenco) {
this.elenco = elenco;
}
}

Per provare il servizio si può utilizzare qualsiasi client REST, per esempio usando PostMan utilizzando l'indirizzo del servizio, per esempio

http://indirizzoIP:8080/ProvaRest/ServiziRest/ServizioRest/echo

si ottiene un saluto mentre se si prova il metodo

http://indirizzoIP:9080/ProvaRest/ServiziRest/ServizioRest/Elenco 

con il json di request

{ "nome":"Alberto" }

si otterrà la risposta HTTP 200 con il json:

{
"elenco": [ "Alberto"],
"esito": "OK"
}

Dato che l'architettura REST è basata su HTTP e il risultato Json si basa su Javascript, se abbiamo a che fare con servizi che interagiscono solo per GET o POST, possiamo creare un semplice consumer con un po' di HTML e Javascript. In una semplice pagina web con un campo di input nel quale inseriremo il valore da inviare al servizio, grazie alla libreria jQuery basta creare una funzione che invochi il servizio al click di un bottone, passando come parametri json il valore inserito dall'utente e, alla fine, mostrare con un alert il risultato.

$(document).ready(function() {
$('#invia').click(function() {
var nome = $('#nome').val();
$.ajax({
url: 'http://localhost:8080/NomeApplicazione/ServiziRest/Elenco',
contentType: 'application/json',
data: '{"nome" : [' + nome + ']}',
type: 'POST',
success: function(data, textStatus, jqXHR) {
alert(data.esito + " " + data.elenco.toString() );
},
error: function(jqXHR, textStatus, errorThrown){
alert(errorThrown);
}
});
});
});

Ovviamente esistono tool molto più evoluti come il programma PostMan, una volta espansione di Chrome poi diventato un programma separato, tramite questo programma è possibile effettuare chiamate HTTP in un tool molto più semplice di una pagina web programmata con Jquery.

Per utilizzare la libreria RestEasy conviene utilizzare maven per scaricare tutte le librerie necessarie, come primo passo bisogna installare maven e configurare eclipse andando sulla voce "Install new software" dentro il menù "Help" e aggiungendo il repository

http://repo1.maven.org/maven2/.m2e/connectors/m2eclipse-mavenarchiver/0.17.2/N/LATEST/

Dopo bisogna installare maven e configurare il progetto con maven: cliccando sul tasto destro sul progetto, nella voce "Maven" è presente la voce "Update project" ricordandosi di selezionare l'opzione "force option". Dopo bisogna creare un nuovo progetto di tipo "Maven project" e selezionare "WebApp" nella scelta dell'Archetype, inserire poi il nome del gruppo e dell'artefatto con i nomi del progetto, alcune versioni di eclipse non creano in automatico la cartella "src/main/java" quindi questa deve essere creata manualmente dentro al progetto. Poi nel file pox.xml basta aggiungere il repository

<repositories>
<repository>
<id>jboss</id>
<url>http://repository.jboss.org/maven2</url>
</repository>
</repositories>

e le dipendenze

<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>2.3.1.GA</version>
</dependency>
<dependency>
<groupId>net.sf.scannotation</groupId>
<artifactId>scannotation</artifactId>
<version>1.0.2</version>
</dependency>

nel file web.xml bisogna ggiungere

<context-param>
<param-name>resteasy.scan</param-name>
<param-value>true</param-value>
</context-param>
<servlet>
<servlet-name>resteasy-servlet</servlet-name>
<servlet-class>
org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>resteasy-servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>

e poi si può creare la classe

package it.alnao.rest.prova;

import javax.ws.rs.*;
import javax.ws.rs.core.*;

@Path("/restService")
public class ProvaRest {
@GET
@Path("/messaggioFisso")
public Response getMessaggioFisso(){
String result="Messaggio fisso";
return Response.status(200).entity(result).build();
}

@GET
@Path(value="/echo/{message}")
public String echo(@PathParam(value="message") String message) {
return "Echo " + message;
}

@Post
@Path("/calcolaDoppio")
@Consumes(MediaType.APPLICATION_JSON) //Input
@Produces(MediaType.APPLICATION_JSON) //Output
public Response provaPost(MainPageRequest p) {
String result = "Product created : " + p.getInput().getSocieta();
return Response.status(200).entity(result).build();
}
}

e la classe bean per gli input e gli output indicando la direttiva XmlRootElement, nonostante siano dei Json è sempre necesaria

package it.prova;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class MainPageRequest {
private MainPageInput input;
public MainPageRequest(MainPageInput input) {
super();
this.input = input;
}

public MainPageRequest(){}

@Override
public String toString() {
return "MainPageInput [input=" + input + "]";
}
public MainPageInput getInput() {
return input;
}
public void setInput(MainPageInput input) {
this.input = input;
}

@XmlRootElement
public static class MainPageInput {
public MainPageInput() {}
private String valore;
public MainPageInput(String valore) {
super();
this.valore = valore;
}
public String getValore() {
return valore;
}
public void setValore(String valore) {
this.valore = valore;
}
@Override
public String toString() {
return "MainPageInput [valore=" + this.valore + "]";
}
}
}

e poi da postMan basta richiamare gli url:

http://localhost:8080/resteasy-servlet/restService/messaggioFisso
http://localhost:8080/resteasy-servlet/restService/echo/adasdaaaa
http://localhost:8080/resteasy-servlet/restService/provaPost

Le annotation java all'interno delle classi dei servizi rest sono:

  • @Path associa i metodi della classe al endpoint della chiamata http, è possibile definirla a livello di classe per tutti i metodi oppure a livello del singolo metodo. Nel nome del metodo possono contenere anche un paramtro come {param}
  • @PathParam definisce il nome di un parametro e il legame con una variabile di ingresso del metodo.
  • @GET : definisce il metodo HTTP del metodo, i valori ammessi sono @GET, @POST, @DELETE e @PUT.
  • @Consumes e @Produces : definiscono il MIME-TYPE dei valori che arrivano al server e delle risposte prodotte. Se non specificati il valore di default "text/html"
    Per quanto riguarda la verione 3 di RestEasy rimando alle varie guide sul web dove sono spiegati i passi per l'installazione e la configurazione della nuova versione del framework, da notare che la seconda e la terza versione non sono compatibili e il codice scritto nella seconda versione deve essere completamente riscritto.

Maven è un progetto di Apache che permette la compilazione il deploy come con Ant, ma aggiunge anche la gestione e il download delle librerie necessarie oltre che alla possiblità di creare in automatico siti predefiniti. Il progetto è scaricabile dal sito ufficiale, nonostante esista la versione senza installer, è consigliato utilizzare la versione con installer nel sistema operativo, dopo l'installazione si può usare comando "mvn -v" per avere le informazioni base

Maven home: C:\Programmi\apache-maven-3.3.9\bin\..
Java version: 1.8.0_60, vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk1.8.0_60\jre
Default locale: it_IT, platform encoding: Cp1252
OS name: "windows 7", version: "6.1", arch: "amd64", family: "dos"

Il file di configurazione di maven è il pom.xml dove POM è l'acronimo di "Project Object Model", un file xml che contiene tutte le informazioni base di un progetto come le dipendenze e la documentazione. Si definiscono come Goal le operazioni (funcion) che possono eseguite sul progetto, le operazioni sono scritte in un linguaggio XML di scripting Jelly.  Tipicamente i comandi maven sono lanciati da riga di comando e sono del tipo

mvn <nome-comando>:[<nome_goal>] [{<parametro>}]

e i principali comando utilizzabili sono

  • clean pulisce il progetto dai componenti compilati
  • compiler compila i file sorgenti
  • deploy esegue il rilascio del pacchetto nel repository remoto se configurato
  • install esegue il rilascio del pacchetto nel repository locale
  • site genera la documentazione
  • archetype genera la struttura di un progetto a partire da un template

I templace standard di maven sono diversi, i più usati sono "maven-archetype-archetype", "maven-archetype-j2ee-simple", "maven-archetype-simple", "maven-archetype-site-simple", "maven-archetype-site" e "maven-archetype-webapp". Per esempio la creazione di un progetto semplice si usa la sintassi:

mvn archetype:generate -DgroupId=it.alnao -DartifactId=nomeModuloApplicazione -DarchetypeArtifactId=maven-archetype-webapp

il risultato è un progettino web con il suo file pom.xml che può essere importato su eclipse con la voce di menù di import selezionado il tipo "Maven Project". Il progetto autogenerato ha creato una pagina jsp con la scritta "Hello World", per compilarla e rilasciarla sul server basta lanciare (nella cartella del progetto) il comando

mvn install

il risultato è un file "nomeModuloApplicazione.war" creato nella cartella target, questo file è ovviamente eseguibile essendo un WAR standard JEE.

Quello che si cerca di fare con Maven è la separazione totale della parte java dalla parte jsp, quindi con Maven si creano due progetti, uno web già creato e uno java con il comando

mvn archetype:generate -DgroupId=it.alnao -DartifactId=javaModule -DarchetypeArtifactId=maven-archetype-quickstart -Dversion=1.0

che genera un semplice ma completo progetto java puro, ripeto che lo scopo è proprio quello di separare la parte web (jsp e HTML) dalla parte Java.
Il problema è che abbiamo due file POM, quindi è consigliato creare un terzo file POM separato dai due precedenti, che importi come moduli le due parti del progetto:

<project xmlns=”http://maven.apache.org/POM/4.0.0″ xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd”> 
<name>Html Maven Project</name>
<modelVersion>4.0.0</modelVersion>
<groupId>it.alnao</groupId>
<version>1.0</version>
<artifactId>html</artifactId>
<packaging>pom</packaging>
<modules>
<module>javaModule</module>
<module>nomeModuloApplicazione</module>
</modules>
</project>

Mentre nel pom della applicazione web (nomeModuloApplicazione) andiamo ad aggiungere il modulo java

<dependency> 
<groupId>it.alnao</groupId>
<artifactId>javaModule</artifactId>
<version>1.0</version>
</dependency>
e possiamo aggiungere altre dipendenze come per esempio il log4j se necessario
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>

Per poi generare il progetto basta lanciare il solito comando

mvn install

dalla cartella dove è stato creato il pom manuale, la prima volta che il progetto viene compilato, maven scarica tutte le librerie dipendenti necessarie alla compilazione quindi potrebbe volerci un po' di tempo la prima volta.  Nella cartella target della compilazione indicata alla fine del comando avremmo due generati: il progetto java puro con il suo jar e il progetto web sempre in formato WAR con all'interno il jar del progetto java e anche il jar di log4j se indicato nel maven.

Il file POM di Maven è fondamentale e deve ben essere configurato quindi ogni voce deve essere ben definita, il tag root è project

  • name definisce il nome del progetto
  • description definisce una breve descrizione
  • url indica l'indirizzo web dedicato del progetto o della compagnia
  • inceptionYear indica l'anno di inizio degli sviluppi
  • version e modelVersion la versione del modello
  • groupId il package standard base dell'applicazione
  • artifactId il tipo di artefatto (html)
  • packaging il tipo di pacchetto (pom di default)

un esempio:

<project>
<name>javaModule</name>
<description>Un modulo Java<description>
<url>http://www.cosenonjaviste.it</url>
<inceptionYear>2012</inceptionYear>
</project>

licenses permette di indicare uno o più licenze del progetto come nome, url e commenti

<licenses>
<license>
<name>Apache 2</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
<comments>A OSS license</comments>
</license>
</licenses>

organization permette di definire l'organizzazione proprietaria del progetto

<organization>
<name>CoseNonJaviste</name>
<url>http://www.cosenonjaviste.it</url>
</organization>

Inoltre nel file POM è possibile definire dei valori fissi chiamati properties

<properties>
<nomeProp>Abc</nomeProp>
<org.springframework.version>3.0.5</org.springframework.version>
</properties>

Per le dipendenze automatiche si usa il tag dependencies che contiene la lista delle dipendenze nei sottotag dependency, per esempio

<dependencies>
<dependency>
<groupId>it.alnao</groupId>
<artifactId>javaModule</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
</dependencies>

Per la gestione delle risorse non java, che il compilatore scarterebbe, è possibile usare il tag resources e resource, dove si definiscono la directory sorgente, il target di destinazione e i filtri di inclusione e esclusione

<resources>
<resource>
<targetPath>META-INF/cosenonjava</targetPath>
<filtering>false</filtering>
<directory>${basedir}/src/main/cosenonjava</directory>
<includes>
<include>config.xml</include>
</includes>
<excludes>
<exclude>**/*.properties</exclude>
</excludes>
</resource>
</resources>

Per importare i plugin basta usare plugins e il tag plugin come elenco delle componenti

<plugins>
<plugin>
<groupId>org.apache.Maven.plugins</groupId>
<artifactId>Maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>

Per la gestione dei repository Maven contiene un plugin dedicato chiamato SCM (Software Configuation Management) questo plugin deve essere importato con

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-scm-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<configuration><connectionType>developerConnection</connectionType></configuration>
</plugin>
</plugins>

Per definire un reposityr nel pom si usa il tag scm, per esempio

<scm>
<connection>scm:svn:http://url_svn/my-project/trunk/</connection>
<developerConnection>scm:svn:http://url_svn/my-project/trunk/</developerConnection>
<tag>trunk</tag>
<url>http://url_svn/my-project</url>
</scm>

e poi è possibile lanciare i comandi di update e checkin, alcuni esempi sono:

mvn scm:update
mvn -Dmessage=Commento_lungo scm:checkin
mvn -Dusername=user -Dpassword=password -Dmessage=Commento_lungo scm:checkin

L'elenco completo di tutti i comandi si trovano nel sito ufficiale di maven

Su Eclipse esiste la possibilità di creare e gestire un progetto Maven senza dover lanciare comandi da riga ed è possibile utilizzare la UI per la configurazione del file POM. Creando un nuovo progetto di tipo "Maven project" infatti è possibile creare un progetto vuoto, nella prima schermata della procedura guidata bisogna ricordarsi di NON selezionare "Create a simple project", nella seconda schermata , grazie ai filtri, si trova e si seleziona il tipo (artefatto) : "maven-archetype-webapp", nella terza schermata è obbligatorio indicare il gruppo, il nome "Artefatto" e il package base della nostra App, il risultato sarà lo stesso di aver lanciato il comando da shell con la differenza che ovviamente ci si trova nella UI di Eclipse. Aprendo il file POM si può utilizzare una semplice interfaccia per modificare il pom che comunque può essere modificato a mano nell'ultima linguetta dell'interfaccia di Eclipse. Da notare che non tutte le versioni di Eclipse aggiungere le librerie base per le servlet quindi bisogna sempre controllare che ci siano le dipendenze

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>2.0</version>
<scope>provided</scope>
</dependency>

E controllare la presenza del plungin di compilazione per la build

<build>
<finalName>MavenWebApp</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

Poi bisogna creare la cartella src/main/java se non esiste (non tutte le versioni di Eclipse la creano), questa è la cartella che andrà a contenere i nostri file java mentre le jsp (WebContent) è la cartella src/main/webapp. Per prova si crea una semplice serlvet:

package it.provaEclipse;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/helloServlet")
public class Servlet extends HttpServlet {
private static final long serialVersionUID = 1L;

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/plain");
resp.getWriter().write("Hello World! Maven Web Project Example.");
}
}

e si procede con la compilazione del progetto corrispondente al "mvn install" da riga di comando: nel menù di Eclipse si apre il menù "Run" e si apre la funzionalità di "Run configurations", qui si seleziona "Maven Build" e si aggiunge una voce con il tasto New.
Nella schermada di configurazione della build si inserisce il nome (per esempio MavenBuild), il goals che deve essere "clean install", e come base directory si seleziona il progetto con il bottone "Workspace"; cliccando sul bottone "Run" parte la compilazione Maven. Da notare che selezionando il file pom, facendo click con il destro del mouse, c'è una voce "Run as" con la possibilità di selezionare quale comando lanciare da un specifico POM come clear o install.

Il sistema base di Maven consiste nell'esistenza di un repository centrale dove le librerie sono importate e grazie al pom vengono gestite le versioni e le dipendenze, l'elenco completo di tutte le librerie disponibili sono sul sito ufficiale mvnrepository.com

Per quanto visto fin ora con Maven, le librerie da configurare sul pom sono:

  • Common lang di apache
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.4</version>
</dependency>
  • TabLib standard
<dependency>
<groupId>org.apache.taglibs</groupId>
<artifactId>taglibs-standard-impl</artifactId>
<version>1.2.1</version>
</dependency>
  • SiteMesh
<dependency>
<groupId>opensymphony</groupId>
<artifactId>sitemesh</artifactId>
<version>2.4.1</version>
</dependency>
  • Struts
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts-core</artifactId>
<version>1.3.10</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts-taglib</artifactId>
<version>1.3.10</version>
</dependency>
<dependency>
<groupId>struts-menu</groupId>
<artifactId>struts-menu</artifactId>
<version>2.4.3</version>
</dependency>
  • Jstl
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.1.2</version>
</dependency>
  • MySql
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version><!--<version>5.1.6</version>-->
</dependency>
  • Jersey
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-server</artifactId>
<version>2.29.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
<version>2.29.1</version>
</dependency>

Git (che nello slang americano significa idiota) è un software di controllo versione distribuito utilizzabile da riga di comando, molto utilizzato in tutto il mondo è andato a sostituire tutti i tipi di repository come CVS e SVN per la sua completezza e la sua facilità d'uso oltre che alla sua completezza e stabilita. E' possibile trovare molti siti come GitLab dove sono disponibili repository "gratuiti" per salvare progetti personali o aziendali. I principali comandi da riga di comando sono:

  • git init : creare un repository dentro la cartella in cui si trova il terminale
  • git clone http://sito.it/utente/nomeprogetto.git: checkout di un repository in una cartella locale cioè scaricare il progetto da un server GIT a locale
  • git clone /percorso/del/repository : checkout di un repository in un percorso locale
  • git chechout development : spostarsi da un branch ad un altro all'interno di un progetto GIT
  • git checkout master : spostarsi da un brach alla radice madre
  • git commit -m "Messaggio per la commit" : committare nella propria area di lavoro GIT quando presente in locale
  • git push : inviare nello stream di GIT quanto committato in GIT
  • git pull : scaricare dal server le nuove versioni se presente
  • git status : visualizzare lo stato del progetto (file in ingresso ed in uscita)
  • git remote -v : visualizzare le informazioni remote
  • git branch -a : elenco dei branch attivi (nel server)
  • git log : avere la sequenza delle commit eseguite (per esempio 1b2e1d63ff)
  • git tag 1.0.0 1b2e1d63ff : marchiare una commit con un tag (la sequenza è data da git log)

Per poter usare GIT su Eclipse, nelle ultime versioni è anche disponibile di default mentre in quelle più vecchie bisogna installando su "Install new software" inserendo il sito "http://download.eclipse.org/egit/updates"

Per configurare un repository su Eclipse basta andare sulla prospettiva GIT e selezionare il pulsante "Clone a GIT Repository" per collegarsi e clonare in locale un repository GIT oppure le altre opzioni disponibili. Una volta configurato il repository locale nei progetti java si hanno a disposizione queste opzioni di GIT: "Share Project" dentro alla voce "Team" per aggiungere il progetto selezionato ad un repository, "Commit" e "Push" per eseguire il commit e il push delle modifiche sul repository.

Dalla vista di GIT è possibile scaricare nel workspace di Eclipse un progetto non già importato selezionando il progetto dal "Working Tree" e facendo il CheckOut.

In questa serie di articoli saranno introdotti e spiegati con esempi pratici alcune tecniche di sviluppo di micro-servizi. Il linguaggio usato sarà Java e la libreria usata sarà solo Spring Boot, gli sviluppi saranno sul SDK Eclipse mentre l'installazione verrà effettuata su Cloud-AWS oltre che ai test in ambiente locale.

I vari esempi riguarderanno il collegamento con varie base dati tra cui MySql e Dynamo, sarà presente anche un esempio su Cognito, il "Identity-Server" di Cloud AWS. Lo scopo finale dei micro-servizi è l'esposizione di API su internet con uno strato di sicurezza, ci saranno esempi manuali e automatizzati di creazione di queste API con anche il CLI di AWS. La scelta di questi componenti è dettata dal fatto che, ad oggi, sono le tecnologie più usate e con le migliori prestazioni al mondo.

Prima di iniziare è necessario scaricare ed installare Eclipse ed avere installata la versione 11 della JRE di Java, questa configurazione prescinde dal sistema operativo usato, tali tools possono essere usati nei sistemi MsWindows, Mac Os oppure GNU Linux. Dopo aver avviato per la prima volta Eclipse bisogna creare un Workspace e bisogna accedere al "Eclipse marketplace" dal menù Help e installare il "EGit" che dovrebbe esserci di default e il "Spring tools 4".

Da notare che nessun web server è necessario, infatti lo sviluppo e i test locali non necessitano di nessun WebServer come apache in quanto è integrato una versione di Tomcat all'interno del plugin di Eclipse mentre l'esecuzione del micro-servizio all'esterno non necessita di nessun server ma basta avere la JRE installata nel sistema per l'esecuzione.

Per creare un micro-servizio esiste un servizio online sul sito start.spring.io oppure tramite il tool direttamente da Eclipse, in queste guide verrà usata questa seconda opzione così da essere indipendenti da qualsiasi sito anche se spring.io è il sito ufficiale è consigliato fare riferimento per la documentazione in caso di necessità.

La procedura guidata per la creazione del progetto può essere lanciata dal menù principale accedendo alla voce new e selezionando il tipo "Spring starter project" dentro la categoria "Spring boot".

Nella prima videata è necessario inserire alcune proprietà obbligatorie

  • Service URL: bisogna lasciare il valore di default https://start.spring.io
  • Name: il nome del micro-servizio e del progetto all'interno del workspace
  • Java: le versioni di Java e il packing, conviene lasciare la versione 11 e il formato jar
  • Group: il gruppo di riferimento in formato java
  • Artifact: il nome dell'artefatto, di solito si usa il nome del micro-servizio
  • Version: la versione del micro-servizio, di default si inizia da 0.0.1-SNAPSHOT
  • Description: la descrizione parlante del micro-servizio
  • Package: il package java base, di solito è composto dal Group.Articaft

Nella seconda videata si seleziona la versione di Spring Boot, la scelta può essere tra 2.6.3 (scelta di default) o la 3 (scelta opzionale sconsigliata ai principianti). Sempre nel secondo passo è possibile aggiungere componenti addizionali, conviene sempre importare "Spring web" e "Spring boot devTools", inoltre è possibile aggiungere librerie specifiche dei vari Database o altre librerie specifiche. Una volta selezionato il bottone "Finish", il programma Eclipse genererà un progetto con il nome indicato con una prima classe application e uno scheletro vuoto del micro-servizio.

Dopo aver creato il progetto è possibile aggiungere la vista "Boot dashboard" dalla voce "Show view" del menù Windows, in questa vista saranno elencati tutti i micro-servizi presenti nel workspace con la possibilità di avviarli e stopparli direttamente da dentro Eclipse grazie al plugin e al WebServer Tomcat messo a disposizione dal plugin stesso.

Nel progetto iniziale viene sempre creata una classe, alcuni cambiano subito nome a questa classe chiamandola genericamente "Application", io suggerisco invece di non farlo per motivi di ordine risulterà più chiara la presenza di una classe con il nome del micro-servizio.

Nella documentazione ufficiale è esposta anche la possibilità di sviluppare API all'interno della classe Application ma questo è fortemente sconsigliato.

Avviando un micro-servizio senza nessuna API, il progetto verrà avviato e sarà possibile vedere nella Console di Eclipse il messaggio

Tomcat started on port(s): 8080 (http) with context path

Tuttavia nessun endpoint risponderà, nemmeno accedendo alla porta 8080 il sistema risponderà perché non c'è nessun "Controller" che espone delle API. Per creare una prima API bisogna creare un "Controller", una classe che espone il metodo:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("api/demoms")
public class DemoMS1Controller {

@GetMapping(value= "/response", produces= "application/json")
public ResponseEntity<String> response(){
return new ResponseEntity<String> ("{'response':'ok'}",HttpStatus.OK);
}

}

In questo esempio la API viene esposta sulla porta 8080 che è la porta di default e l'endpoint sarà "api/demos/riposta". Da un punto di vista tecnico non è necessario posizionare la classe in un package specifico, bastano infatti le annotation per creare le API, tuttavia è buona norma abituarsi a mantenere ordine all'interno dei package, quindi è consigliato creare un sottopacakge "controller" all'interno del package dove si trova la classe Application.

Per avviare il microservizio da Eclipse basta selezionarlo nella vista "Boot dashboard" e avviarlo, nei log è presente tutto lo stack e la conferma di avvio nella porta. Per prova basta accedere via browser al

http://localhost:8080/api/demoms/response 

per vedere la risposta del micro-servizio.

Se, in fase di avvio, nella console compare un messaggio di errore "Indirizzo già in uso" significa che la porta 8080 è già usata nel sistema, nel prossimo articolo sarà indicato come configurare la porta EndPoint del micro-servizio.

Il file di configurazione viene creato in fase di creazione del progetto "application.properties", tale file è un file properties quindi con quel tipo di formato, in alcuni progetti è possibile trovare anche il formato yaml, in questi articoli invece verrà usato solo il properties. Di default la porta è 8080 ma è possibile personalizzare la porta con la il dettaglio:

server.port=5051

Mentre la gestione dei log può essere inserita nel file, per esempio:

logging.file.name=./target/log/ArtSrv.log 
logging.file.max-size=1MB
logging.pattern.file=%d{ dd-MM-yyyy } [%thread]%-5level %logger{36} - %msg%n
logging.pattern.console=%d{dd-MM-yyyy HH:mm:ss} %-5level - %msg%n
logging.level.root= WARN
logging.level.org.springframework= INFO

Per creare manualmente il jar bisogna avere installato maven scaricabile dal sito Apache ufficiale. Una volta installato si può verificare con il comando

mvn -version

dove si può vedere anche la versione di Java, che dovrebbe essere la 11 se si dispone dell'ultima versione. Se c'è una versione diversa bisogna ricreare il micro-servizio indicando la corretta versione di Java. Una volta pronti per la compilazione basta lanciare il comando

mvn clean install

per creare il jar che sarà disponibile all'interno della cartella target assieme poi ad altri file che per ora possono essere ignorati. Per avviare manualmente il micro-servizio dal jar basta lanciare il comando

java -jar target/*.jar

per vedere che il micro-servizio verrà avviato e si vedrà direttamente in riga di comando alcuni log tra cui il solito

25-01-2022 15:06:33 INFO - Tomcat started on port(s): 5051 (http) with context path ''

e così si può provare il micro-servizio disponibile usando la porta indicata nel file di configurazione.

Per eseguire un logging efficace all'interno dei micro-servizi si può usare la libreria standard slf4j. Un esempio completo del nostro metodo con un semplice log è:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("api/demoms")
public class DemoMS1Controller {
Logger logger = LoggerFactory.getLogger(DemoMS1Controller.class);

@GetMapping(value= "/response", produces= "application/json")
public ResponseEntity<String> response(){
logger.debug("response api called");
return new ResponseEntity<String> ("{\"response\":\"ok\"}",HttpStatus.OK) ;
}
}

Importante notare che potrebbe non comparire il log nei file o nella console se nel file di configurazione del micro-servizio non si è impostato un livello specifico per il pacakge della classe
Per esempio basta aggiungere:

logging.level.it.alnao.examples.controller= DEBUG

per avere i log a livello debug. Altrimenti il log potrebbe non essere salvato nel file.

Il servizio Elastic BeanStalk presente su AWS permette di avviare micro-servizi su istanze con pochi click e nessuna configurazione sistemistica. Si rimanda alla guida di AWS per la creazione della utenza con il piano gratuito e dell'uso dei servizi base di AWS. In questo articolo sarà usata una istanza t2micro che rientra nel piano gratuito di AWS quindi non prevede nessun costo ma bisogna prestare attenzione ai passi eseguiti.

Prima di tutto, entrati nel servizio di Elastic BeanStalk, bisogna creare una applicazione impostando il nome e il tipo di piattaforma ovviamente selezionare Java alla versione 11. E' possibile selezionare anche Java alla versione 8 se si è usato quella versione della JRE. Alla fine della pagina si deve selezionare "Carica il tuo codice" e si può selezionare dal proprio pc il file jar del micro-servizio. L'alternativa è creare un S3 pubblico con il file caricato ma la scelta è sconsigliata. Una volta creata l'applicazione si vedrà una console dove nella console AWS compaiono i passi eseguiti, come la creazione del security group e lo storage su S3.

Una volta avviata l'applicazione bisogna si può notare sulla console di EC2 che è presente una nuova istanza e che questa è avviata correttamente con un suo IP. Prima di provare la API su quel indirizzo bisogna modificare il security group di quella istanza perchè di default AWS crea una regola solo sulla porta 80 mentre il micro-servizio è esposto su un'altra porta, quindi bisogna eliminare la regola sulla porta 80 e si deve creare una regola di incoming sulla porta specifica del micro-servizio, nel esempio è la 5051.
Terminata questa configurazione la API risponderà al EndPoint:

http://<IndirizzoIP>:5051/api/demoms/response

Per evitare costi inutili conviene sempre cancellare la istanza EC2 e la applicazione Elastic BeanStalk, altrimenti si rischiano addebiti indesiderati se dovesse rimanere attiva dopo la prova per diverso tempo.

Prima di procedere con la creazione dell'immagine bisogna assicurarsi di aver installato nel sistema il demone/server Docker, indispensabile per la creazione e per l'esecuzione del container dell'immagine. E' possibile seguire l'articolo dedicato dedicato a Debian per avere una installazione all'interno del proprio sistema.

Per creare l'immagine bisogna definire le sue caratteristiche all'interno di un file di testo con nome Dockerfile e all'interno bisogna definire i passi da eseguire che nel nostro semplice esempio saranno: scaricare l'immagine di GNULinux con Java, copiare il jar del microservizio e poi eseguirlo. 

# select base image to run 
FROM openjdk:11-jre-slim
# copy the packaged jar file into our docker image
COPY target/ExampleMicro1-0.0.1-SNAPSHOT.jar /ExampleMicro1.jar
# set the startup command to execute the jar
CMD ["java", "-jar", "/ExampleMicro1.jar"]

Proprio per natura dei micro-servizi, il comando java è semplice e non c'è la necessità di includere altre librerie o aggiungere comandi speciali, in caso di necessità comunque è possibile aggiungere i parametri  al comando java come la configurazione della memoria.

Dopo aver creato il file configurazione basta eseguire il comando per creare l'immagine

$ docker build -t examplemicro1:1.0-SNAPSHOT .

e poi, per avviare un container partendo dall'immagine creata, basta eseguire il comando

$ docker run -d -p 5052:5051 examplemicro1:1.0-SNAPSHOT

per poi verificare sulla porta 5052 in url

localhost:5051/api/demoms/response

da notare, che per puro esercizio, abbiamo mappato la porta 5001 esposta dal servizio avviato nel container verso la porta 5002 del sistema, questo mapping può essere evitato se si decide che la porta esposta non deve essere cambiata.

Dopo aver avviato l'istanza si possono eseguire i comandi per gestire le immagini e i container, nell'ordine: elenco delle istanze avviate, visualizzazione dei log, stop di una istanza, pulizia dei container non avviati, lista delle immagini e rimozione di una immagine).

$ docker ps
$ docker logs <container_id>
$ docker stop <container_id>
$ docker container prune
$ docker image ls
$ docker image rm <image_id>

Per il carico della immagine docker su AWS bisogna usare il servizio di Elastic Container Repository (ECR), per farlo bisogna aver installato la CLI di AWS e poi bisona eseguire il caricamento su ECR:

$ aws ecr create-repository --repository-name examplemicro1 --region eu-west-1

Il risultato sarà un json di risposta con il codice repositoryUri che sarà indispensabile nei passi successivi

{
"repository": {
"repositoryArn": "arn:aws:ecr:eu-west-1:XXXXXXX:repository/examplemicro1",
"registryId": "XXXXXXX",
"repositoryName": "examplemicro1",
"repositoryUri": "XXXXXXX.dkr.ecr.eu-west-1.amazonaws.com/examplemicro1",
"createdAt": "2022-03-22T14:12:53+01:00",
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": false
},
"encryptionConfiguration": {
"encryptionType": "AES256"
}
}
}

Successivamente bisogna creare il tag per l'immagine con il comando usando il valore del repositoryUri

$ docker images --filter reference=examplemicro1
$ docker tag <immagineID> <repositoryUri>

E poi si carica l'immagine su AWS grazie al tag appena creato

$ aws ecr get-login-password | docker login --username AWS --password-stdin XXXXXXX.dkr.ecr.eu-west-1.amazonaws.com/examplemicro1
$ docker push XXXXXXX.dkr.ecr.eu-west-1.amazonaws.com/examplemicro1

dopo un po' di tempo l'immagine viene caricata e basta controllare nella console web di AWS per verificare il corretto caricamento dell'immagine su AWS ECR. Successivamente bisogna creare una regola nel servizio IAM attivando con "Allow" l'accesso alla immagine appena creata usando il path ARN creato in precedenza.
Per avviare l'immagine caricata basta accedere al servizio di Elastic Container System (ECS) e avviare una istanza di tipo custom selezionando l'immagine caricata in precedenza.

In fase di configurazione bisogna ricordarsi di configurare il port mapping aprendo la porta 5001 o qualunque porta esposta dall'immagine, in assenza della regola di rete la porta non sarà esposta.
L'esposizione viene eseguita dal servizio di Load Balancer quindi l'url di accesso sarà del tipo

http://<id-id>.eu-west-1.elb.amazonaws.com:5001/api/demos/response

Per eliminare l'istanza bisogna procedere direttamente dal ECS mentre l'immagine depositata su ECR può essere cancellata con il comando:

$ aws ecr delete-repository --repository-name examplemicro1 --region eu-west-1 --force

dove si ottiene un json di risposta di conferma.

E' possibile definire in maniera veloce un metodo speciale che venga chiamato solo all'avvio del microservizio, questa tecnica è spesso usata per inserire un log in una tabella oppure per inizializzare/pulire una base dati. Per rendere automatico l'avvio di un metodo basta definire una metodo nella classe application e usare l'annotation Bean in modo che sia inizializzato all'avvio del micriservizio, a titolo di esempio in questo codice vengono usati anche gli argomenti di avvio del microservizio e visualizzati in un log.

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class ExampleMicro4mongoApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleMicro4mongoApplication.class, args);
}

@Bean
public CommandLineRunner demo(ExampleMicro4mongoReposiroty r) {
return args -> {
r.save(new ExampleMicro4mongoEntity("1","Avviato il " + new Date()));
System.out.println("Elenco elementi nel ExampleMicro4mongo ");
for ( var el : r.findAll() ) {
System.out.println(el.toString());
}
};
}
}

Oltre al classico Eclipse con l'estensione STS (Spring tools suite) è possibile usare il programma Visual studio code di Microsoft. Personalmente lo trovo molto più scomodo per l'uso di un progetto con molti microservizi o sistemi misti ma molti lo usano per la semplicità e la stabilità del programma. Per poterlo usare bisogna installare le tre estensioni base: "Spring initializer java support", "Spring boot tools" e "Spring boot dashboard" che risultano indispensabili per sviluppare e testare i microservizi da VsCode.

Per creare un progetto da zero basta andare nella console di VsCode con CTRL+T e inserire

Create java project

A questo punto bisogna seguire gli step: il tipo "Spring boot", la libreria maven, la versione, il pacakge, il nome, la versione di java e il tipo di compilazione tra jar e war. Al termine chiede la posizione della cartella dove salvare il progetto che si viene a creare, con questa procedura si crea il pom.xml per il compilatore e una classe application vuota quindi il microservizio non farà nulla se avviato.

Una seconda via per creare un progetto da zero è installare l'estensione "Maven for java" e cliccando con il tasto destro compare la voce "Create a maven project". anche in questo caso è possibile selezionare il tipo "spring boot", la versione e il package di riferimento. Questo secondo progetto oltre alla classe application verrà creato con una classe di configurazione e una classe controller che definisce due metodi: il classico Hello e un metodo per fare la somma di due numeri.

Il framework Spring mette a disposizione un completo supporto per la gestione dei dati verso i database SQL e NoSQL, per esempio grazie a JDBC oppure librerie specifiche come Hibernate. Lo standard di Spring Boot è l'implementazione di un repository interfaccia e l'utilizzo di metodi per la auto-creazione di query. L'interfaccia principale javax.sql.DataSource fornisce un metodo i collegamenti ad una base di dati tramite un "DataSource" definito come URL e le credenziali di accesso. L'import di JPA avviene nel file pom con l'aggiunta della libreria:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Poi per il collegamento ad una base dati, nel file di properties dell'applicazione bisogna censire l'url di collegamento e le credenziali, nel formato yaml:

spring:
datasource:
url: "jdbc:mysql://localhost/test"
username: "dbuser"
password: "dbpass"

oppure nel formato properties:

spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=admin
spring.datasource.password=blmp202!

Il modello standard di JPA prevede 3 componenti base: la entity, il repository e il service, questo modello rappresenta la base del modello MVC dove la classe entity corrisponde al model mentre le classi repository e service corrispondono al controller. Per quanto riguarda la view, il microservizio esprorrà dei servizi API-REST. La classe del model, deve avere la annotation Entity così il framework in automatico gestirà correttamente i campi, ogni campo deve avere la annotation corrispondente Id oppure Column a seconda del tipo

import javax.persistence.*;
@Entity
public class DemosComuniBLM {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String denominazione;
@Column(nullable = false)
private String regione;
... //metodi getter e setter

da ricordare che ogni proprietà, che corrisponde al campo della base dati, deve avere i metodi setter e getter standard. Lo strato di repository è molto semplice ed è una interfaccia che estende la classe Repository del framework con la possibilità di definire dei metodi specifici, per esempio:
Nella definizione della classe ci sono due meta-tipi: il model creato poco sopra e il tipo della chiave primaria, nell'esempio un Long:

public interface DemosIRepositoryComuni extends Repository<DemosModelComuni, Long> {
List<DemosModelComuni> findAll();
}

Da notare che in questo esempio avendo usato findAll, il framework va a costruire in automatico la query di selezione quindi non è necessario scrivere nessuna query SQL. Lo strato di servizio, tipicamente chiamato Service, innietta con autowired la classe repository al proprio interno e poi usa i metodi standard di JPA per gestire i dati, in questo caso si hanno i quattro metodi del CRUD più i medoti specifici del servizio:

@Service
public class DemosServiceComuni {
@Autowired
DemosIRepositoryComuni repo;
public List<DemosModelComuni> getAll() {
return repo.findAll();
}
}

Tuttavia provando questo esempio si genera un errore causato dalla mancanza della libreria di collegamento con il database. L'esempio base non funziona se non si imposta una libreria specifica della base di dati, in quanto mancano ancora alcune configurazioni, queste dipendono dal tipo di DBMS che si utilizza, nei prossimi articoli saranno esposti esempi per ogni tipo di base dati.

Uno dei Database relazionali più usati al mondo è MySql, per usare questo DBMS bisogna prima di tutto censire il driver nel file di configurazione

spring.datasource.driver-class-name = com.mysql.jdbc.Driver

e bisogna controllare di avere la dipendenza nel pom.xml

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>

Poi nella classe entity del Model bisogna definire il nome dello schema e il nome della tabella all'interno della tabella MySql con una specifica annotation table

@Table(name= "comuni_italia", schema = "nome_db")

Per provare il codice nella classe controller basta definire un metodo per l'esposizione dell'API che ritorna l'elenco degli elementi recuperato dal servizio

@Autowired
private DemosServiceComuni service;

@GetMapping(value= "/listaComuni", produces= "application/json")
public ResponseEntity<List<DemosModelComuni>> listaComuni(){
List<DemosModelComuni> l=service.getAll(null);
if (l==null || l.size()==0)
return new ResponseEntity<List<DemosModelComuni>> ( new ArrayList<DemosModelComuni>() ,HttpStatus.NOT_FOUND);
return new ResponseEntity<List<DemosModelComuni>> ( l ,HttpStatus.OK);
}

e poi basta accedere al endpoint

localhost:5051/api/demoms/listaComuni

per vedere la lista ritornata dal servizio recuperata dalla base dati.

La classe repository rappresenta il collegamento tra il model, nella classe entity, e il servizio, nella classe service. Il tipo generico Repository non crea nessun metodo specifico mentre sono previste interfacce dedicate al alcuni DBMS o casi d'uso reali. Il più semplice caso d'uso è il CRUD ed è possibile usare la classe CrudRepository per ereditare i quattro metodi (creazione, selezione, aggiornamento e cancellazione) senza dover scrivere una riga di codice.

import org.springframework.data.repository.CrudRepository;
public interface DemosIRepositoryComuni extends CrudRepository<DemosModelComuni, Long> {
//non è necessario definire nessun metodo specifico
}

Mentre il service cambia per chiamare il metodo findAll messo a disposizione del framework

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DemosServiceComuni {
@Autowired
DemosIRepositoryComuni repo;

public Iterable<DemosModelComuni> getAll() {
return repo.findAll();// findAll();
}
}

Mentre il controller che espone le API non dovrebbe cambiare a meno dei tipi leggermente diversi (tra List e Iterator).

Per il salvataggio di dati basta definire nel service un metodo che usi il metodo save del repository di default

public DemosModelComuni save(DemosModelComuni el) {
return repo.save(el);
}

e nella classe controller si può definire una nuova API di tipo Post per l'esposizione del servizio di aggiornamento

@PostMapping(value= "/aggiornaComune", produces= "application/json")
public ResponseEntity<DemosModelComuni> aggiornaComune(@RequestBody DemosModelComuni comune){
DemosModelComuni c=service.save(comune);
return new ResponseEntity<DemosModelComuni>(c, HttpStatus.CREATED);
}

da notare che il servizio non fa distinzione tra operazione di inserimento di un nuovo elemento oppure modifica di uno esistente, perché è la libreria del framework che gestisce questa logica all'interno della classe CrudRepository. In tutti i successivi servizi saranno usati metodi sempre più specifici in modo da scrivere meno codice e concentrarsi sulla logica di business che dovrà essere implementata all'interno della classe di Service.

Se si dispone di un dabatase Postgres l'architettura mette a disposizione il driver dedicato, in fase di creazione del progetto su Ecplise basta selezionare il driver di PostgreSQL oppure si può aggiungere manualmente la pom.xml il riferimento alla libreria:

<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

Rispetto alla versione con MySql non è necessario cambiare nulla, infatti l'interfaccia CRUD è compatibile con tutti i DBMS relazionali basati su SQL. In molti esempi è possibile trovare l'uso dell'interfaccia base JPA ma conviene usare la CRUD che è leggermente più evoluta. L'unica differenza rispetto alla versione MySql è proprio l'uso del DMBS differente con le diverse regole di SQL. Una delle differenze maggiori che bisogna sempre ricordare è l'uso delle sequence in Postgres. Il DBMS Postgres definisce differenti schema all'interno dello stesso dababase, questo ovviamente è dichiarato nell'annotation Table

@Entity
@Table(name= "articoli", schema = "public")
public class ExampleMicro3postgresEntity { ... }

Inoltre Postgres utilizza delle sequence dedicate che devono essere dichiarate nella classe Entity nel campo corrispondete, per esempio un campo id deve essere definito con:

@Id
@GeneratedValue( generator="sq_articoli")
private Long id;

cioè all'interno della annotation specifica bisogna dichiarare quale è la sequence interna al DB.

L'intero progetto è disponibile su GitHub

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro03postgres

Se si dispone di una base date Mongo, l'architettura mette a disposizione una libreria e un driver dedicato, in fase di creazione del progetto basta selezionare il driver corretto oppure basta aggiungere il tag al pom.xml:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

Rispetto alle versioni SQL bisogna definire il document (al posto della tabella e la collection.

import java.util.List;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "elenco")
public class ExampleMicro4mongoEntity {
@Id
private String _id;
private String nome;
}

La classe Repository estende un repository specifico (in questo esempio con un metodo speciale)

import org.springframework.data.mongodb.repository.MongoRepository;
public interface ExampleMicro4mongoReposiroty extends MongoRepository<ExampleMicro4mongoEntity,String>{
public ExampleMicro4mongoEntity findByNome(String nome);
}

Mentre le classi service e controller non hanno nulla di diverso visto che le classi Repository ereditano dalla stessa interfaccia. La vera differenza è il file di configurazione che vuole parametri specifici:

spring.data.mongodb.authentication-database: admin
spring.data.mongodb.database: collectiondemo
spring.data.mongodb.host: localhost
spring.data.mongodb.username: alnao
spring.data.mongodb.password: bello
spring.data.mongodb.port: 27017

Dove bisogna definire tutti i dati di collegamento al dabatase compreso il database e il authentication-database di cui non si può fare a meno in caso di base dati con Mongo.

Il framework Spring Boot gestisce nativamente la configurazione basata su Java, è possibile configurare una origine XML di configurazione con la annotation @ImportResource ma la via più veloce e consigliata è quella di definire le configurazioni tramite una class con la annotation @Configuration.

Questa tecnica permette di non dover gestire file Xml fisici ma di usare classi java che eventualmente possono caricare dati dai file di properties in maniera nativa. Per esempio:

@Configuration
public class ExampleMicro5dynamoConfig {
@Value("${amazon.dynamodb.profile}")
private String amazonDynamoDBProfile;
@Bean
public Object obj() {
return xxx;
}
}

In questo caso la classe configuration va a creare un bean (in questo caso non sviluppato), da notare che nell'esempio viene recuperato una properties dal file architetturale con l'annnotation @Value così da poter essere usata in tutta la classe. Questo metodo è usato per definire classi di configurazioni di varie liberire che necessitano configurazioni più dettagliate rispetto ad un semplice file di properties, spesso infatti le librerie hanno delle classi Builder che faviriscono la creazione di bean specifici.

Un esempio pratico è la possibile configurazione di un database di sviluppo locale con una classe specifica

@Bean
//@ConditionalOnProperty( name = "usemysql", havingValue = "local")
@ConditionalOnMissingBean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/myDb?createDBIfNotExist=true");
dataSource.setUsername("mysqluser");
dataSource.setPassword("mysqlpass");
return dataSource;
}

Un altro esempio pratico è la libreria per DynamoDB fornisce un metodo specifico per creare gli End-Point di configurazione, il metodo introdotto nell'articolo precedente è del tipo:

@Bean
public AmazonDynamoDB amazonDynamoDB() {
return AmazonDynamoDBClientBuilder
.standard()
.withEndpointConfiguration(getEndpointConfiguration())
.build();
}

lo sviluppo di questo metodo è molto più semplice di quanto possa essere la gestione di file XML statici.

Per il collegamento ad una base dati AWS DynamoDB il percorso è leggermente più complicato perché i driver di collegamento sono sono disponibili direttamente ma bisogna modificare il POM.xml aggiungendo le librerie proprietarie di AWS e dei sistemi cloud.

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<version>1.12.215</version>
</dependency>
<dependency>
<groupId>com.github.derjust</groupId>
<artifactId>spring-data-dynamodb</artifactId>
<version>5.1.0</version>
</dependency>

Per lo sviluppo di servizi basati su Dynamo è possibile trovare molti altri tipi di esempi con altre librerie ma la versione 5.1 di derjust è la più stabile, successive versioni potrebbero andare in contrasto con le più recenti versioni di Spring Boot. La classe Entity deve definire alcune annotation per definire il nome della tabella sul Dynamo, l'elenco dei campi e le chiavi.

@DynamoDBTable(tableName = "ProvaJavaSpringBoot")
public class ExampleMicro5dynamoEntity {
@DynamoDBHashKey(attributeName = "id")
@DynamoDBAttribute
private String id;

@DynamoDBAttribute
private String nome;

@DynamoDBAttribute
private String cognome;

Il repository deve avere la annotation EnableScan e può definire dei metodi supplementari per esempio contare quelli con un nome specifico:

@EnableScan
@Repository
public interface ExampleMicro5dynamoRepository extends CrudRepository<ExampleMicro5dynamoEntity, String> {
@EnableScanCount
long countByNome(String nome);
}

Le classi service e controller sono identici ai casi precedenti. Se provato così il servizio non riuscirà ad avviarsi perché non è ancora stato definito quale è il servizio a cui collegari per questo bisogna definire alcune proprietà: 

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
amazon.dynamodb.profile=default

e in particolare la prima proprità evita che la classe DataSourceAutoConfiguration generi errore perché manca un vero e proprio diver JDBC. Tuttavia per potersi collegare dal proprio pc o da un server al servizio AWS di Dynamo è necessario definire una configuration class con le credenziali programmatiche, argomento esposto nel prossimo articolo.

Per potersi collegare dal proprio pc o da un server al servizio AWS di Dynamo è necessario definire una configuration class con le credenziali programmatiche, per fare questo bisogna creare le credenziali di tipo programmatico nel servizio IAM di AWS e poi creare una configuration class nel progetto.
La configuration class deve definire un provider di credenziali, è possibile definire un profilo tamite AWS CLI e poi usare quel tipo di connessione con una semplice classe:

@Configuration
@EnableDynamoDBRepositories(basePackages = "it.alnao.examples",
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = EnableScan.class))
public class ExampleMicro5dynamoConfig {
@Value("${amazon.dynamodb.profile}")
private String amazonDynamoDBProfile;
@Bean
public AmazonDynamoDB amazonDynamoDB() {
AWSCredentialsProvider credentials =
new ProfileCredentialsProvider(amazonDynamoDBProfile);
AmazonDynamoDB amazonDynamoDB
= AmazonDynamoDBClientBuilder
.standard()
.withCredentials(credentials)
.build();
return amazonDynamoDB;
}
}

Da notare che è obbligatorio definire la annotation EnableDynamoDBRepositories dove si devono collegare tutte le classi Repository per permettere alla classe Repository di usare le credenziali configurate.
In alternativa all'uso del profilo AWS CLI è possibile indicare nella classe le chiavi programmatiche IAM, se si devide questa strada è insipensabile usare un oggeto di tipo CredentialsProvider e nelle classi bisogna indicare la region visto che il servizio Dybani non è globale.

@Bean
public AmazonDynamoDB amazonDynamoDB() {
return AmazonDynamoDBClientBuilder
.standard()
.withEndpointConfiguration(endpointConfiguration())
.withCredentials(awsCredentialsProvider())
.build();
}
public AwsClientBuilder.EndpointConfiguration endpointConfiguration() {
return new AwsClientBuilder.EndpointConfiguration(endpoint, region);
}
public AWSCredentialsProvider awsCredentialsProvider() {
return new AWSStaticCredentialsProvider(new BasicAWSCredentials(accesskey, secretkey));
}

Il modello architetturale REST permette la comunicazione tra sistemi remoti con lo scopo di trasmettere dati in maniera sicura e robusta. Per molti versi Java e REST non vanno molto d'accordo in quanto la rappresentazione dei dati prevista è in Json a differenza del più amato SOAP che prevede il formato XML per la rappresentazione e lo scambio di dati. In questo articolo non verranno confrontante le due tecnologie di comunicazioni visto che Java Script Boot permette l'uso di entrambi i protocolli e formati, in questi articoli l'approccio sarà quello standard previsto dal framework:

  • i servizi saranno esposti con risorse REST, e non metodi come invece avviene in un servizio basato su SOAP
  • il formato per rappresentare le risorse esposte è il JSON, anche se è possibile cambiare velocemente il formato in XML
  • il protocollo usato sarà HTTP (e HTTPS)
  • viene usato il paradigma del "stateless" quindi non deve essere usato il sessione visto che il server è privo di memoria ed ogni richiesta verrà gestita in maniera indipendente

Tutto questo viene gratis se i microservizi sono studiati bene con l'indipendenza intrinseca tra servizi e tra risorse. Per la configurazione dei protocolli e dei formati verranno usate annotation messe a disposizione dal framework così che il programmatore non debba preoccuparsi di questi aspetti tecnici che potrebbero essere complessi. In particolare bisogna ricordarsi di aggiungere al progetto in fase di creazione o nel file pom.xml la dipendenza dalla libreria: 

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

La libreria spring-boot-starter-web mette a disposizione l'annotation @RestController che indica che una classe definisce una risorsa REST, convenzionalmente il nome di queste classi terminano per "Controller" proprio per ricordare che si tratta di un Controller API. Per completare la definizione del servizio esposto, basta aggiungere l'annotation @RequestMapping("api/url") che descrive quale è l'end-point della risorsa esposta come API. All'interno delle classi controller si può eseguire l'AutoWired di una o più classi service in modo da gestire, infatti convenzionalmente le classi Controller non hanno nessuna logica di business al proprio interno ma fungono solo da end-point e descrivono solo la gestione degli stati di risposta HTTP (200,400, ecc...), qualunque sia la logica di business viene delegata alle classi di tipo service.

La API più semplice è generare un valore costante, per descrivere questo basta creare un metodo con la annotation @GetMapping che indica che il metodo java sarà richiamabile dalla API con il metodo HTTP-GET all'url indicato

@GetMapping("/hello")
public String hello() {
return "Hello";
}

in questo esempio ovviamente non c'è un ritorno completo in quanto manca il tipo di ritorno HTTP. Per creare una risposta HTTP standard si può usare la classe ResponseEntity e il valore HttpStatus.OK che rappresenta come costante il valore 200 corrispondete al OK del protocollo HTTP. Per esempio:

@GetMapping("/hello")
public ResponseEntity<String> hello(
@RequestParam(value = "name", defaultValue = "World") String name) {
String ritorno="Hello";
return new ResponseEntity<String> ( ritorno ,HttpStatus.OK);
}

La risposta di questo secondo esempio può sembrare molto simile al prima ma è molto diverso se analizzato con un client HTTP. Per passare un parametro in querystring dal chiamante alla API si usa l'annotation @RequestParam nei parametri dei metodi, per esempio:

//http://localhost:8080/api/demoms/helloWorld?name=Liubov
@GetMapping("/helloWorld")
public ResponseEntity<String> helloWorld(
@RequestParam(value = "name", defaultValue = "World") String name) {
return new ResponseEntity<String> ( String.format(template, name) ,HttpStatus.OK);
}
private static final String template = "Hello, %s!";

In questi casi i valori di ritorno sono sempre delle stringhe fisse e non hanno un formato Json che ormai è uno standard universale per le applicazioni web, per definire un ritorno in formato json si usa la proprietà produces nella annotation del @GetMapping, per esempio:

@GetMapping(value="/helloWorld", produces= "application/json") 

Anche in questo caso non si noteranno differenze rispetto alla versione perchè si è usata una stringa come tipo di ritorno, al contrario se si definisce come ritorno una classe java il ritorno sarà un json che il framework andrà a costruire in automatico:

@GetMapping(value="/helloWorld", produces= "application/json") 
//http://localhost:8080/api/demoms/helloWorld?name=Liubov
public ResponseEntity<Messaggio> helloWorld(
@RequestParam(value = "name", defaultValue = "World") String name) {
return new ResponseEntity<Messaggio> ( new Messaggio (String.format(template, name)) ,HttpStatus.OK);
}
public class Messaggio{
String messaggio="";
public Messaggio(String m) {this.messaggio=m;}
public String getMessaggio() { return messaggio; }
public void setMessaggio(String messaggio) {
this.messaggio = messaggio; }
}

Il ritorno di questo esempio ritorna un json standard:

{"messaggio":"Hello, World!"}

Da notare che in questo ultimo esempio la classe Messaggio deve essere standard con i metodi setter e getter altrimenti il framework non riesce ad eseguire il "cast" da oggetto java a formato json, è possibile usare lombok per generare questi metodi in automatico.

Per definire una risorsa che ritorna una lista convenzionalmente si usa un ritorno Iterable di oggetti java standard. A titolo di esempio si presenta un ritorno OK 200 e NOT_FOUND 400 diversificati a seconda dell'input e ritorna una lista con due elementi di cui uno fisso e uno dinamico.

@GetMapping(value= "/lista", produces= "application/json")
public ResponseEntity<Iterable<Messaggio>> lista(@RequestParam(value = "name", defaultValue = "World") String name) {
if ("Alberto".equals(name) ){
return new ResponseEntity<Iterable<Messaggio>> (
new ArrayList<Messaggio>() ,HttpStatus.NOT_FOUND);
}
ArrayList<Messaggio> l=new ArrayList<Messaggio>();
l.add( new Messaggio (name));
l.add( new Messaggio ("Alberto"));
return new ResponseEntity<Iterable<Messaggio>> ( l ,HttpStatus.OK);
}

In questo esempio il ritorno è molto semplice visto che è una lista in formato json con due elementi:

[{"messaggio":"Liubov"},{"messaggio":"Alberto"}]

Come già visto nei precedenti esempi, questa tecnica è usata soprattutto per ritornare un elenco ritornato da un service che va a recuperare i dati da una fonte JPA o da altre fonti, per esempio:

@GetMapping(value= "/lista", produces= "application/json")
public ResponseEntity<Iterable<XXXXEntity>> lista(){
Iterable<XXXXEntity> l=service.findAll();
if (l==null )
return new ResponseEntity<Iterable<XXXXEntity>> (
new ArrayList<XXXXEntity>() ,HttpStatus.NOT_FOUND);
return new ResponseEntity<Iterable<XXXXEntity>> ( l ,HttpStatus.OK);
}

Tipicamente non sono usati parametri o input in formato json come di solito viene usato per il salvataggio di dati con risorse eseguibili con metodi POST.

Le API che sfruttano REST usano per best-practices diversi metodi la comunicazione tra client e server, per la modifica e la cancellazione si possono usare i metodi standard PUT, POST e DELETE previsti proprio dal protocollo HTTP. Per l'uso di del metodo POST si usa la annotation @PostMapping che possiede le proprietà del nome della risorsa e l'indicazione del formato della risposta come json.

Tipicamente, con i medoti POST e PUT, non si usano parametri in querystring ma per best-practices si usa un oggetto json come interfacia del metodo, per descrivere questo si usa la annotation @RequestBody e, tipicamente, si usa una classe Entity se è possibile usare la stessa struttura tra interfaccia API e interfaccia con la base dati. Se le interfacce tra API e DB fossero diverse, per varie esigenze, è necessario definire una classe Entity separata con una classe che esegua il mapping tra Entity dell'interfaccia Rest e Entity dell'interfaccia con la base dati, mapping da eseguire nella classe service e NON nella classe Controller che non dovrebbe mai contenere logica di business. Un esempio (semplicissimo) di metodo post è:

@PostMapping(value= "/save", produces= "application/json")
public ResponseEntity<ExampleMicro5dynamoEntity>
save(@RequestBody ExampleMicro5dynamoEntity el){
el=service.save(el);
return new ResponseEntity<ExampleMicro5dynamoEntity> (
el ,HttpStatus.OK);
}

Per i metodi PUT e DELETE esisono le corrispettive annotation @PutMapping e @DeleteMapping. Se si vuole creare una unica risorsa per diversi metodi è possibile indicare con solo la barra \ all'interno della proprietà value, in questo modo i metodi java saranno diversi con differenti annotation e, sopratutto, eseguibili con differenti metodi HTTP.

Quando si esegue un micro-servizio è quasi indispensabile validare i dati di input che arrivano per evitare spiacevoli errori e impedire ad un chiamante (anche malevolo) di inserire dati sporchi nella base dati.
Per questo il framework si basa interamente sulle validazioni standard java definire sul package javax.validation che può essere importato nel pom.xml

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>

questa libreria definisce una lista molto numerosa di annotation usabili nelle classi Entity per definire le regole di validazione, per esempio

@Entity
public class UserEntity {
@NotNull(message = "UserName cannot be null")
@Size(min = 5 , message = "Lunghezza almeno 5 caratteri")
@Column(nullable = false)
private String userName;
...
}

in questi esempio nella classe entità è definita una proprietà di una colonna e una regola di validazione NotNull, in questo modo se l'API espone un metodo che prevede questo oggetto in request, alla invocazione del metodo viene prima eseguita una validazione del campo ed interrotto il metodo in caso di validazione negativa con il messaggio di errore visualizzato. Per attivare la validazione dei metodi esposti con PostMapping (o qualsiasi altra annotation) bisogna aggiungere l'annotation @Validated al parametro del metodo java esposto.

@PostMapping(value= "/save", produces= "application/json")
public ResponseEntity<ExampleMicro5dynamoEntity> save(
@Validated @RequestBody ExampleMicro5dynamoEntity el){

Le annotation di validazione più utili sono:

  • @NotNull validare se l'oggetto non vale null
  • @Size valida la lunghezza da un minimo ad un massimo, applicabile a String, Collection, Map, and array properties.
  • @Min valida il valore se maggiore di un valore minimo
  • @Max valida il valore se minore di un valore massimo
  • @Email valida se l'oggetto è una mail in formato valida
  • @Digits(integer=6, fraction=2) valida il valore numerico con il numero (massimo) di cifre intere e cifre decimali
  • @DecimalMin("1.00") valida se l'oggetto è un decimal con un valore minimo
  • @Pattern(regexp="..") valida se la espressione regolare è rispettata

Da notare che la validazione può essere usata anche alle liste in modo da eseguire la validazione di tutti gli elementi, per esempio:

List<@NotBlank String> preferences;

Si rimanda alla documentazione ufficiale per maggiori dettagli riguardo a tutti i validatori che spesso risultano indispensabili in una infrastruttura basata sui microservizi.

E' possibile definire delle regole custom di validazione implementando un metodo specifico con una classe annotation. Prima di tutto bisogna creare la annotation, da Eclipse si può creare il nuovo oggetto selezionando i parametri

- Retetion = RunTime
- Target = Field
- Add @Documented

Il codice sorgente della classe prevede tre metodi tra qui la definizione del messaggio che sarà visualizzato in caso di validazione errata.

@Documented
@Retention(RUNTIME)
@Target(FIELD )
@Constraint(validatedBy=ExampleMicro5CustomValidator.class)
public @interface ExampleMicro5CustomValidatorAnnotation {
String message() default "Validazione custom errata"; //message
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

Poi bisogna creare la classe che implementa il metodo, in questo semplice esempio la validazione sarà che il valore sia pari, in caso di valore dispari il validatore restituirà errore

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
Pattern pattern=Pattern.compile("^\\d*[02468]$");
if (value==null || !pattern.matcher(value).matches() ){
return false;
}
return true;
}
}

Per attivare la validazione basta aggiungere la custom annotation alla proprietà interessata, per esempio:

@ExampleMicro5CustomValidatorAnnotation
private String id;

In questo modo è possibile definire qualsiasi tipo di validazioni con qualsiasi tipo di test specifico.

Il modello architetturale finora presentato è perfetto se il modello dati della database è lo stesso dei servizi rest chiamato dal client, tuttavia i due modelli MAI coincidono perfettamente anche solo per la rappresentazione dei dati, per esempio il client e la base dati hanno formati di date diversi o rappresentazioni di numeri decimali in formati differenti. Per risolvere il problema della differenza di strutture dati tra repository e controller (che corrispondono a databse e client) è necessario definire un modello dati per il repository come Model già visto in precedenza e un modelo dati per il client chiamato DTO.

L'acronimo del pattern DTO signica Data Transfer Object e viene usato per definire classi java di tipo Model ma usata solo nelle classi controller che espongono i servizi Rest.

Per eseguire il mapping tra classi DTO e classi Entity bisogna definire un mapper per entrambi versi di conversione, tra le tante librerie disponibili è possibile usare la ModelMapper con l'aggiunta nel pom di:

<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.0.0</version>
</dependency>

Grazie a questa libreria, nelle classi servici, è possibile eseguire il mapping tra classe DTO e Model in automatico se tutte le proprietà coincidono per nome tipo:

ModelMapper modelMapper = new ModelMapper();
OrderDTO orderDTO = modelMapper.map(order, OrderDTO.class);

Le proprietà che differiscono per nome e tipo vengono ignorare dalla libreria, un esempio classico è: la base dati definisce un campo dataCreazione ma il client, per motivi vari, definisce il valore nome del campo con dataCreaz. Per eseguire il mapping anche dei campi non corrispondenti, bisogna definire una classe Configuration dove si definisce il model mapper e i campi specifici

@Configuration
public class ModelMapperConfig{
@Bean
public ModelMapper modelMapper(){
ModelMapper mm=new ModelMapper();
mm.getConfiguration().setSkipNullEnabled(true);
//definizione di quali mapper hanno proprietà diverse
mm.addMapping(type1Mapper);
//si possono definire più mapping se presenti più classi
//mm.addConverter(aConverter);
return mm;
}
//definizione mapping specifico
PropertyMap<Model,ModelDto> type1Mapper = new PropertyMap<Model,ModelDto>(){
protected void configure(){
map().setDataCreazione(source.getDataCreaz() );
}
}
}

nel service poi si può usare il mapper:

// injection del ModelMapper 
@Autowired
private ModelMapper modelMapper;
//metodo richiamato dal controller
ModelDto selByCodArt(String id){
Model articoli=repository.getById(id); //JPA
return this.ConvertToDto(articoli)
}
//call al metodo di conversione di un oggetto
ModelDto convertToDto(Model a){
ModelDto objDto=null;
if (a!=null){ //evitare i nullPointerExc
objDto=modelMapper.map(a , ModelDto.class);
}
return aDto;
}

In caso di lista si può usare l'operatore freccia o un ciclo classico, per esempio:

List<ModelDto> ret = a.stream().map(
element -> modelMapper.map( element , ModelDto.class )
).collect(Collectors.toList() );
return ret;

Per maggiori approfondimenti si rimanda alla guida ufficiale di ModelMapper.

Il CORS (Cross-Origin Resource Sharing) è un meccanismo standard previsto dal protocollo HTTP per controllare nel server quali client possono eseguire i metodi esposti tramite API REST e viene configurato come header delle chiamate HTTP dal client che poi il server deve validare. Il metodo più semplice è permette a qualunque chiamante con qualsiasi metodo, per sviluppare questo basta aggiungere a tutte le classi Controller la annotation

@CrossOrigin(origins = "*", allowedHeaders = "*")

Invece per permettere una sola orginale chiamante si può indicare la sorgente

@CrossOrigin(origins = "http://localhost:8080")

Da notare che si possono aggiungere alle classi Controller oppure direttamente ai metodi delle risorse in modo da esporre metodi a specifici chiamanti. In alternativa all'uso delle annotation è possibile definire un bean di configurazione (o un metodo nella classe Application), in questo modo è possibile definire per ogni API quali sono le origini che possono risolvere le risorse esposte:

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api")
.allowedOrigins("http://localhost:8080");
}
}
}

Per approfondimenti si rimanda alla documentazione ufficiale di spring che parla della gestione del Cors. 

Esistono librerie automatiche per la creazione di documentazione delle API sviluppate con Spring Boot, la più famosa è Swagger e in questo articolo viene introdotta la versione tre che risulta la più recente e attuale, per configurare la libreria basta aggiungere nel file pom la dipendenza della libreria

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.4</version>
</dependency>

e dopo bisogna aggiungere nel file di configurazione la properties

springdoc.api-docs.path=/api-docs

In questo modo il framework crea delle API di documentazione in formato json all'url

http://localhost:5051/api-docs

e la stessa documentazione ma in formato yaml all'url

http://localhost:5051/api-docs.yaml

Inoltre la nuova versione di Swagger crea un sito navigabile con tutta la documentazione all'indirizzo

http://localhost:5051/swagger-ui.html

In questi esempi si fa riferimento ad una API che viene eseguita su un server locale esposto nella porta 5051. Un altro esempio per la documentazione è possibile usare anche open-api, una valida alternativa a swagger e spesso usata perché alcune versioni di Spring Boot possono non essere compatibili con le versioni di swagger.

La libreria Swagger crea la documentazione in automatico ma è possibile aggiungere note e descrizioni testuali usando alcune annotation previste dalla libreria, l'elenco completo delle annotation è disponibile nella documentazione ufficiale https://swagger.io/specification/, in questo articolo sono brevemente elencati i principali casi d'uso. Per aggiungere nella annotazione la descrizione di una API si usa la operation e ApiResponses per descrivere il ritorno di una API, per esempio le get:

@Operation(summary = "Get a list", tags = "getList")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Found elements",
content = { @Content(mediaType = "application/json",
schema = @Schema( implementation = ArrayList.class))
}
),
@ApiResponse(responseCode = "404", description = "None element found",content = @Content) })
}

Per aggiungere nella annotazione un parametro di input si aggiungere la description di un parameter:

@GetMapping("/{id}")
public Book findById(@Parameter(description = "id of book to be searched") @PathVariable long id) {
return repository.findById(id).orElseThrow(() -> new BookNotFoundException());
}

Se una API ha in request un oggetto strutturato, è possibile descriverne il tipo di Body in request con la annotation RequestBody con la descrizione e lo schema:

@Operation(summary = "Save a new object or update existing if id already exist")
@PostMapping(value= "/save", produces= "application/json")
public ResponseEntity<ExampleMicro5dynamoEntity> save(
@RequestBody(
description = "Element of ExampleMicro5dynamoEntity",
required=true,
content = { @Content(mediaType = "application/json",
schema = @Schema(implementation = ExampleMicro5dynamoEntity.class)) }
)
@Validated @RequestBody ExampleMicro5dynamoEntity el
){
el=service.save(el);
return new ResponseEntity<ExampleMicro5dynamoEntity> ( el ,HttpStatus.OK);
}

La documentazione generata però risulterebbe vuota visto che non sono presenti informazioni nella classe Entity, si possono aggiungere informazioni sul modello dati direttamente nella classe Model con la annotation Schema dove si indica un nome e la descrizione, per ogni campo è possibile indicare le caratteristiche come tipo, lunghezza, obbligatorietà, ecc...

@Schema(example = "Any string I want", 
name = "ExampleMicro5dynamoEntity element",
description = "Model element of ExampleMicro5dynamoEntity")
@DynamoDBTable(tableName = "ProvaJavaSpringBoot")
public class ExampleMicro5dynamoEntity {
@DynamoDBHashKey(attributeName = "id")
@DynamoDBAttribute
@Schema(format = "id", required = true ,type = "String")
private String id;
@Schema(required = true ,type = "String" , minLength = 5)
@DynamoDBAttribute
private String nome;
@DynamoDBAttribute
private String cognome;

La documentazione generata in automatico con Swagger è priva di strato di sucrezza e tutti coloro che conoscono l'EndPoint posso accedere a tale elemento. Per evitare accessi indesiderati è possibile aggiungere una classe di configurazione per gestire la sicurezza delle pagine generate da Swagger.
Prima di tutto bisogn aggiungere la libreria security del framework di Spring Boot

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Poi bisogna definire una classe con la annotation EnableWebSecurity in modo da aggiungere la sicurezza alla swagger-ui generata in automatico dalla libreria della documentazione:
package com.javainuse.configuration;

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/swagger-ui/**", "/javainuse-openapi/**").permitAll()
.anyRequest().authenticated()
.and().httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javainuser")
.password(passwordEncoder().encode("javainusepwd"))
.authorities("ADMIN");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

In questo semplice esempio si crea una regola sulla risorsa "swagger-ui" della API con una autenticazione base con user e password che in questo caso sono indicate direttamente nella classe per semplicità. Alla classe application bisogna indicare con SecurityScheme un nome simbolico della configurazione il tipo di configurazione basic e il tipo.

@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "Employees API",
version = "2.0", description = "Employees Information"))
@SecurityScheme(name = "javainuseapiname", scheme = "basic",
type = SecuritySchemeType.HTTP, in = SecuritySchemeIn.HEADER)
public class SwaggerSpringDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SwaggerSpringDemoApplication.class, args);
}
}

Alla fine bisogna configurare la API per attivare la sicurezza.

@RestController
@SecurityRequirement(name = "javainuseapiname")
public class EmployeeController { ... }

Con questo esempio si è configurata nella API uno strato di sicurezza che si attiva solo nella parte esposta dallo swagger con l'istruzione:

.antMatchers("/swagger-ui/**", "/javainuse-openapi/**").permitAll()

Quando si avvia un progetto con Spring boot, nei log e nella console di Eclipse compare un logo chiamato banner, questo è personalizzabile grazie a dei parametri specifici. Esistono moltissimi siti web dove creare "ASCII art" e basta cercare sui motori di ricerca "online spring boot banner generator" per trovare molti siti che possono creare cose smili alle fan art anche molto complesse, per esempio il sito: devops.datenkollektiv.de ha creato il banner:

  ,---.   ,--. ,--.  ,--.                       ,--.   ,--.   
/ O \ | | | ,'.| | ,--,--. ,---. `--' ,-' '-.
| .-. | | | | |' ' | ' ,-. | | .-. | ,--. '-. .-'
| | | | | | | | ` | \ '-' | ' '-' ' .--. | | | |
`--' `--' `--' `--' `--' `--`--' `---' '--' `--' `--'
${application.title} ${application.version}
Powered by Spring Boot ${spring-boot.version}

se usate siti di buona qualità vi verranno aggiunti anche alcuni parametri dopo la art come il titolo e la versione dell'applicazione.  La art deve essere salvata in un file di testo con estensione txt contenente solo il testo da visualizzare nel banner e il file deve essere salvato all'interno delle cartelle resources del progetto. Il file di configurazione deve essere modificato per aggiungere il riferimento al file appena salvato

spring.banner.image.location=classpath:banner.txt

E' possibile anche colorare il testo visualizzato nei log e nella console con un semplice parametro per ogni riga

${Ansi.GREEN}

Potrebbe sembrare un cosa da poco ed effettivamente lo è!

L'architettura Spring Boot mette a disposizione la gestione base delle cache a livello di API rest e può essere configurata con pochissimo codice in un micro-servizio già esistente, basta aggiungere nel pom la dipendenza:

<!-- CACHE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

E bisogna ricordarsi di aggiungere una annotation nella classe Application per rendere attiva la cache:

@EnableCaching
public class Application {

Nelle classi Service poi bisogna abilitare la cache a livello di servizio con la annotation:

@Service
@CacheConfig(cacheNames= {"ExampleMicro5dynamoService"} )
public class ExampleMicro5dynamoService {

Poi nei singoli metodi della classe service è possibile aggiungere la cache a livello generale o indicando una chiave per evitare che venga salvato un elemento sopra un altro usando un campo univoco e identificativo degli elementi, per esempio:

@Cacheable
public Iterable<ExampleMicro5dynamoEntity> findAll(){
return repository.findAll();
}
@Cacheable(value="ExampleMicro5dynamoServicefindById", key="#id", sync=true)
public Optional<ExampleMicro5dynamoEntity> findById(String id){
return repository.findById(id);
}

questo sistema ha due difetti:

  • la cache non viene mai pulita quando vengono inseriti o cancellati elementi
  • se ci sono più istanze dello stesso micro-servizio queste sarebbero separate con problemi di univocità e logiche di cancellazione

per entrambi i problemi andremmo a vedere nei prossimi due articoli come risolvere i due problemi.

Un esempio completo e funzionante è disponibile nel git:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro05dynamo

La gestione della pilizia della cache può essere gestita con vie filosofia che possono essere usate anche assieme: usare i metodi di modifica di dati per invalidare la cache oppure esporre una api che esegua lo svuotamento della cache. Per pulire la cache da un metodo di modifica dati basta aggiungere una annotation nei metodi dei servizi, per esempio il metodo save di un elemento deve cancellare la cache dell'elenco:

@Caching(evict = {
@CacheEvict(cacheNames="ExampleMicro5dynamoService", allEntries=true),
@CacheEvict(cacheNames="ExampleMicro5dynamoServicefindById", key="#el.id"),
})
public ExampleMicro5dynamoEntity save(ExampleMicro5dynamoEntity el){
return repository.save(el);
}}

Nel secondo caso, per esporre una API che esegua la pulizia della cache si può esporre una API nel controller:

@GetMapping("clearCache")
public void clearCache() {
service.clearCache();
}

e nella classe service è possibile usare l'oggetto cacheManager messo a disposizione dall'architettura per pulire la cache.

@Autowired
CacheManager cacheManager;
public void clearCache() {
cacheManager.getCache("ExampleMicro5dynamoService").clear();
cacheManager.getCache("ExampleMicro5dynamoServicefindById").clear();
}

Da notare che ogni cache configurata in questo modo ha un nome quindi risulta facile gestire e pulire selettivamente solo cache di determinati servizi. Il grande difetto di questo sistema è che con più istanze avviate dello stesso microservizio, ogni istanza gestisce in autonomia la cache quindi il refresh di una NON andrebbe ad pulire le altre così, per risolvere questo problema si opta per una libreria di gestione condivisa: HazelCast.

Il problema della libreria base di Spring boot è che se lo stesso micro-servizio è in esecuzione ha la propria cache autogestita, ma se ci sono più istanze avviate di un micro, ogni istanza gestisce in autonomia la propria cache e non sarebbe valida la regola di cancellazione e gestione della cache stessa. Per esempio avviando due istanze del micro-servizio della lezione precedente, se entrambe le cache fossero piene e la prima verrebbe cancellata per l'inserimento di un elemento, la seconda istanza non si accorgerebbe di questo evento e continuerebbe ad usare la cache con dati imprecisi.

Per evitare questo bisogna usare il metoco IMDQ (In Memory Data Grid) per condivisione tra diversi micro-servizi e la libreria hezelcast. Prima di tutto bisogna aggiungere nel pom.xml la dipendenza della libreria

<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast-all</artifactId>
<version>4.2.5</version>
</dependency>

poi bisogna creare un file di configurazione hazelcast.yaml il cui contenuto deve essere:

hazelcast:
network:
join:
multicast:
enabled: true

Per configurare la gestione della cache si usano le annotation di Spring Boot già viste nei precedenti articoli. L'unica configurazione che bisogna fare, nelle ultime versioni, è una classe di configurazione

@Configuration
public class ExampleMicro6cacheConfig {
@Bean
public Config config() {
Config cfg = new Config();
final TcpIpConfig tcpIpConfig = cfg.getNetworkConfig().getJoin().getTcpIpConfig();
tcpIpConfig.setEnabled(true);
tcpIpConfig.setMembers(List.of("127.0.0.1"));
return cfg;
}
}

per configurare da quali IP c'è la possibilità di collegarsi alla cache condivisa, usando 127.0.0.1 ci si riferisce al IP locale. Per provare questa tecnica si possono lanciare più istanze dello stesso micro-servizio su porte diverse con il comando:

java -jar target/*.jar --server.port=5052

Lanciando una API nel primo micro-servizio si popolerà la cache che verrà usata dalle successive chiamate dello stesso e di tutti gli altri eseguiti e collegati. Nei log dei micro-servizi di dovrebbero vedere l'elenco dei membri che accedono alla cache per esempio due istanze:

Members {size:2, ver:6} [
Member [192.168.1.XX]:5701 - XXXXXXXXXXXXXXXXXX this
Member [192.168.1.XX]:5702 - YYYYYYYYYYYYYYYYYY
]

questo log viene aggiornato man mano che i micro-servizi si collegano e si scollegano dal sistema. Ovviamente quando l'ultimo del gruppo viene fermato, tutta la cache viene distrutta dalla JVM e dal Garbage Collector di Java. 

Un esempio completo può essere trovato nel progetto scaricabile dal GIT

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro06cache

In questa serie di articoli vengono illustrate le due vie possibili per gestire l'autenticazione e l'autorizzazione dei chiamanti alle API esposte con un microservizio sviluppato con Java Spring Boot. Il promo metodo illustrato è la via più semplice: l'autenticazione semplice tramite username e password chiamata anche BasicAuth ed è il metodo più semplice ma anche quello meno usato in quanto comporta che la password venga inviata dal client al server ogni chiamata API, questa tecnica ha come grande punto a favore la semplicità di sviluppo ma ha tre grandi punti negativi:

  • la password viene inviata dal client al server ad ogni chiamata della API con una maggiore possibilità che la password possa essere intercettata
  • il client deve conservare la password per tutto il tempo e inviarlo ad ogni chiamata a API
  • la validazione delle credenziali deve avvenire ad ogni chiamata API con la conseguente esecuzione del metodo ogni chiamata causando uno spreco di risorse per l'esecuzione di ogni API
  • il client non può sapere nativamente quali API sono autorizzate

Il secondo metodo è il più evoluto e il più usato al mondo: il token JWT (Json web token), tecnica che contiene diversi componenti:

  • un servizio per la gestione degli utenti che non viene esposto esternamente ma usato solo dal secondo servizio
  • un servizio che esegue la validazione delle credenziali e la generazione del token
  • un servizio che esegue la validazione del token inviato dal client alle API con anche la verifica dell'autorizzazione per l'esecuzione di una specifica API

Con entrambi questi metodi comunque bisogna si possono fare pochissime modifiche alle API classiche svilupate con un Microservizio. In ogni caso tutte le tecniche si basano sulla libreria security del framework che deve essere aggiunta nel pom.xml

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Per basic-auth si intende la tecnica di validare username e password ad ogni esecuzione della API, con le conseguenze positive e negative già descritte nel precedente articolo. Prima di tutto bisogna definire una classe AuthEntryPoint per gestire l'errore che l'API risponderà al client in caso di errore di autenticazione

public class AuthEntryPoint extends BasicAuthenticationEntryPoint{
private static String REALM="REAME";//nome del reame
@Override
public void commence(final HttpServletRequest request
,final HttpServletResponse response
,final AuthenticationException authException) throws IOException{
String msg="Userid e/o password non valide";
logger.warn("Errore"+authException.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader("WWW-Authenticate", "Basic realm=" + getRealmName());
PrintWriter writer=response.getWriter();
writer.println(msg);
}
@Override
public void afterPropertiesSet(){
setRealmName(REALM);
super.afterPropertiesSet();
}
}

Poi bisogna definire una classe Configuration per la gestione delle credenziali, questo semplice esempio le credenziali di username e password sono scritte nel codice in un metodo che alternativamente può richiamare un service e recuperare username e password da una base dati:

@Configuration
@EnableWebSecurity
public class ExampleMicro7basicAuthConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {//creatore di password
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
UserBuilder user = User.builder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(user
.username("root")
.password(new BCryptPasswordEncoder().encode("difficile"))
.roles("ADMIN")//elenco ruoli
.build()
);
manager.createUser(user
.username("alnao")
.password(new BCryptPasswordEncoder().encode("bello"))
.roles("USER")//elenco ruoli
.build()
);
return manager;
}
}

Infine bisogna definire il metodo che definisce la regola di validazione

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic().realmName(REALM).authenticationEntryPoint(getBasicAuthEntryPoint())
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}

Al fine del corretto funzionamento bisogna aggiungere due metodi ulteriori per la gestione del entry point (che richiama la AuthEntryPoint) e la gestione del metodo http OPTION:

@Bean
public AuthEntryPoint getBasicAuthEntryPoint() {
return new AuthEntryPoint();
}

Questo esempio basta per far funzionare l'autenticazione su tutte le api esposte dal micro-servizio.

Nell'esempio precedente la basic-auth è abilitata per tutte le API esposte e tutti i metodi hanno lo stesso controllo di autorizzazione: qualunque utente autenticato è autorizzato a consumare qualunque API del micro-servizio. Per introdurre il concetto di autorizzazioni personalizzate bisogna prima di tutto notare che ogni utente ha un ruolo nella sua definizione, nell'esempio ADMIN e USER. Per limitare le api ai ruoli bisogna modificare la classe configure indicando per ogni classe di API il ruolo specifico

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/demoms/lista").hasAnyRole("USER","ADMIN")
.antMatchers("/api/demoms/amministrativa").hasRole("ADMIN")
.antMatchers(HttpMethod.POST).hasAnyRole("ADMIN")
.anyRequest()
.authenticated()
.and()
.httpBasic().realmName(REALM).authenticationEntryPoint( getBasicAuthEntryPoint())
.and()
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS)
;
}

In questo semplice esempio abbiamo la API lista che è condivisa ad entrambi i ruoli mentre la api amministrativa è limitata a solo gli utenti con ruolo ADMIN. Il sistema permette anche di definire ruoli al singolo metodo HTTP, nell'esempio viene aggiunta una regola che limita l'uso del metodo POST a solo gli utenti che hanno il ruolo ADMIN. Per superare il problema del CORS viene infatti definita una regola speciale per permettere a qualunque chiamante di usare il metodo HTTP-OPTIONS per accedere alle API

@Override
public void configure(WebSecurity web) throws Exception{
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
}

Nell'esempio è stato inserito un service ad una piccola base dati su Mongo con una tabella utenti con i campi:

private String id;
private String userId;
private String password;
private String attivo;
private List<String> ruoli;
...

Questo micro-servizio verrà usato nei successivi per la gestione degli utenti in maniera amministrativa e per la generazione e la validazione del token JWT.

Un esempio completo di questo esempio può essere trovato al respository git:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro07basicAuth

Per creare un microservizio che generi i token JWT bisogna partire dalle librerie necessarie censendo nel pom.xml  

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

poi nei file di properties bisogna definire la chiave segreta del token, in modo che possa essere usata in fase di cifratura del token e poi il microservizio di validazione del token userà la stessa per decrifrare. Per esempio:

jwt.secret=alnaobello

nel microservizio sarà necessario il service e il repository per collegarsi alla base dati e recuperare le informazioni dell'utente in quanto questo microservizio, prima di generare il token verifica le credenziali (username & password) quindi il repository deve definire un metodo per cercare l'utente:

public interface ExampleMicro8gestJwtRepository extends MongoRepository<ExampleMicro8gestJwtEntity,String> {
public List<ExampleMicro8gestJwtEntity> findByUserId(String userId);
}

mentre il service espone il metodo per ritornare le info dell'utente ma non esegue la validazione delle credenziali.

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ExampleMicro8gestJwtEntity user=null;
List<ExampleMicro8gestJwtEntity> users = repository.findByUserId(username);
if (users!=null) {
if (users.size()>0) {
user=users.get(0);
}
}
if (user != null){
return new User(user.getUserId(),
new BCryptPasswordEncoder().encode(user.getPassword()) ,
new ArrayList<>()
);
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}

Le credenziali arrivano al microservizio nella classe JwtRequest e vengono validate dal metodo authenticate del manager definito in una classe SecurityConfiguration che definisce la regola di validazione:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable() // We don't need CSRF for this example
// dont authenticate this particular request
.authorizeRequests()
.antMatchers("/api/demoms/authenticate").permitAll()
// all other requests need to be authenticated
.anyRequest().authenticated().and()
// make sure we use stateless session; session won't be used to
// store user's state.
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}

La classe RequestFilter infine implementa la logica della verifica delle credenziali e la generazione del token. Sono presenti le classi TokenUtil ed EntryPoint che forniscono i metodi di utilità per il Filter.

L'esempio completo e funzionante è disponibile al gitHub:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro08gestJwt

Cosa manca a questo esempio? La validazione di username e password che verrà introdotta in un successivo articolo. Mentre è presente una piccola risorsa API per eseguire la validazione del token generato ma questo componente verrà introdotto in un altro articolo.

Nell'esempio precedente manca la autenticazione, si fa direttamente nella classe controller nel metodo della risorsa usando un authenticationManager

@Autowired
private AuthenticationManager authenticationManager;
@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(
@RequestBody ExampleMicro8gestJwtRequest authenticationRequest)
throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword() ));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new ExampleMicro8gestJwtResponse(token));
}

Creare classe ExampleMicro8gestJwtSecurityConfiguration basica con definizione di 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ExampleMicro8gestJwtSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
...
}

Importante è che nel service si usi la classe User di framework Spring e su valorizzi con username e password in questo modo non bisogna definire il metodo di confronto ma verrà usato quello standard del framework. Infatti nel service c'è l'istruzione:

return new User(user.getUserId(), 
new BCryptPasswordEncoder().encode(user.getPassword( ) ) ,

Inoltre è importante notare che nel metodo "configure" della classe SecurityConfiguration viene definita questa regola

.antMatchers("/api/demoms/authenticate").permitAll( )

per permettere a qualsiasi chiamate di accedere alla risorsa API authenticate senza dover usare il token JWT, altrimenti la risorsa sarebbe bloccata per quei chiamanti che non hanno ancora il token perché non ancora autenticati (la prima chiamata di autenticazione non ha ancora il token).

Riprendendo il precedente esempio che genera il token, per validare token bisogna aggiungere regola nella classe configuration per forzare validazione ad ogni chiamata di qualsiasi risorsa esposta dei controller:

@Autowired
private ExampleMicro8gestJwtEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private ExampleMicro8gestJwtRequestFilter jwtRequestFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers("/api/demoms/authenticate").permitAll()
// all other requests need to be authenticated
.anyRequest().authenticated().and()
// make sure we use stateless session; session won't be used to store
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(
jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}

Bisogna poi definire una classe Fileter : componente per filtrare quelle richieste che hanno il token valido, e bisogna definire anche una classe EntryPoint proprio per gestire i gli endpoint sicurizzati, si rimanda al GitHUB dell'esempio per il codice, la risorsa API si può definire come semplice metodo:

@GetMapping(value ="/lista")
public ResponseEntity<List<String>> lista(){
ArrayList<String> l=new ArrayList<String>();
l.add("ok");
return new ResponseEntity<List<String>> ( l ,HttpStatus.OK);
}

Effettuando una chiamata alla API senza token verrà ritornato un errore lanciato dalla classe EntryPoint. 

Per eseguire un test completo basta eseguire la API "authenticate" per ottenere il token da poi passare alla API "lista" aggiungendo il JWT come oggetto Bearer, provando senza token o con un token scaduto il servizio ritorna un errore HTTP di tipo 401.

Negli esempi precedenti abbiamo usato il token JWT per validare la credenziali e la scadenza del token stesso ma spesso le applicazioni di frontend usano il token per gestire i regole e le autorizzazioni alle singole API, infatti all'interno del token c'è una lista di "roles" valorizzata dal servizio che genera il token e che si può usare per passare informazioni al frontend. Rispetto al microservizio sviluppato in precedenza si deve modificare la classe di utilità ExampleMicro8gestJwtTokenUtil dove basta aggiungere la put delle regole:

public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", userDetails.getAuthorities() );
return doGenerateToken(claims, userDetails.getUsername());
}

in questo modo le regole saranno incluse nel token JWT e il client potrà usarle per validare le sue risorse. Ovviamente lato backend questa modifica è ininfluente e il token viene sempre validato con le sue regole.

Una delle funzionalità indispensabili per un buon progetto e riuscire a far lavorare e collaborare diversi micro-servizi che spesso non sono nemmeno nello stesso server ma che appartengono ad una stessa infrastruttura e che sono studiati e progetti per dialogare in maniera veloce e sicura. Prima di tutto bisogna che tutti i micro-servizi abbiano lo stesso livello di sicurezza cioè lo stesso metodo di autenticazione e autorizzazione, ovviamente con la possibilità di avere autorizzazioni diverse, nel nostro caso useremo sempre il token JWT firmato dalla stessa chiave e all'interno saranno definiti ruoli diversi per ogni microservizio.

La libreria usata per creare micro-servizi sincroni sarà Open Feign mentre per esecuzioni asincrone sarà implementata una logica di scrittura e lettura tramite una Coda MQ. Per poter funzionare al meglio, una infrastruttura a micro-servizi, avrà bisogno di un orchestratore, un monitor e un sistema di bilanciamento, per queste tre esigenze sarà usato Spring Boot Cloud e tutte le sue potenzialità.

L'esempio che verrà esposto in questi articoli vedrà un piccolo progetto con lo scopo di creare un piccolo ecosistema di micro-servizi queste caratteristiche:

  • micro-servizio magazzino per recuperare la quantità disponibile di un prodotto
  • micro-servizio per gestire l'elenco dei prodotti da una tabella
  • recupero della quantità in magazzino dal micro-servizio dei prodotti
  • micro-serzivio per gestire il token con Spring Boot Cloud
  • micro-servizio per l'inserimento di un ordine con chiamate asincrono al magazzino
  • chiamata ad un micro-servizio esterno per simulare l'invio di un avviso al cliente
  • per la gestione delle credenziali viene usato lo stesso micro-servizio già analizzato nell'esempio  precedente (ExampleMicro8gestJwt)

Tutti questi esempi saranno disponibili nel gruppo ExampleMicro9feign e i successivi esempi nel GitHub

https://github.com/alnao/JavaSpringBootExample/

In questi esempi, viene usato una base dati Mongo per non avere la necessità di studiare e definire una struttura della base dati rigida, si consiglia l'uso di un database relazionale SQL-based se necessario.

In questi esempi viene usata la versione 2.7.4 di Spring Boot perché versioni più recenti come la 3 potrebbero non funzionare bene con alcuni componenti del Feign e del Spring Cloud, la versione del framework è configurabile nel file pom.xml nella sezione parent:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
</parent>

Con questo codice si evitano problemi di compilazione e soprattutto problemi a runtime in fase di avvio del micro-servizio.

La sottolibreria OpenFeign di SpringBoot permette di eseguire la chiamata da una API ad un'altra API anche se queste sono micro-servizi diversi in esecuzioni su istanze diverse (potenzialmente anche su server diversi se le regole di rete permettono l'instradamento delle chiamate). Il primo esempio è un piccolo metodo che dato un codice prodotto ritorni la quantità di prodotti presenti in magazzino.

Per semplicità (e per pigrizia) userò il progetto ExampleMicro8gestJwt come magazzino dove mi è bastato aggiungere il metodo

@GetMapping(value ="/magazzino/{codArt}")
public ResponseEntity<List<String>> magazzino(
@RequestHeader("Authorization") String authHeader,
@PathVariable("codArt") String codArt)
{
System.out.println("magazzinoAPIEsterna codArt=" + codArt);
ArrayList<String> l=new ArrayList<String>();
String s="17";
//recupero della quantità da una base dati
l.add(s);
return new ResponseEntity<List<String>> ( l ,HttpStatus.OK);
}

Questo semplice metodo (incompleto del repository per leggere la tabella) sarà il metodo chiamato dal secondo micro-servizio ExampleMicro9feign che andremo a creare come già fatto per tutti gli altri con in aggiunta nel pom.xml la dipendenza alla libreria:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.5</version>
</dependency>

con la premura di importare tutte le classi della validazione JWT presenti nel pacakge security del progetto visto nei precedenti articoli perché sarà necessario rendere sicuri tutti i micro-servizi e sarà necessario che il micro-servio chiamante passi il token JWT al chiamato in mondo che entrambi possano validare il token come misura di sicurezza. Per attivare la libreria basta aggiungere nella classe application la annotation:

@EnableFeignClients

e poi bisogna creare una classe interfaccia che specifichi il nome e l'url del micro-servizio chiamato, inoltre bisogna definire un metodo della risorsa da chiamare:

@FeignClient(name="ExampleMicro8gestJwt", url="localhost:5051/api/demoms")
public interface ExampleMicro9feignMagazzinoInterface{
//metodi API
@GetMapping(value ="/magazzino/{codArt}")
public ResponseEntity<List<String>> magazzino(
@RequestHeader("Authorization") String authHeader,
@PathVariable("codArt") String codArt
);
}

in questo semplice esempio si usa una List<String> come tipo ritornato dal servizio, se ci fossero tipi complessi bisognerebbe creare una classe DAO serializzabile da usare come ritorno del servizio. Come semplice esempio, si può aggiungere nella classe controller l'autowired della classe interfaccia che poi sarà disponible per essere chiamato all'interno di una risorsa esposta nel controller stesso:

@Autowired
ExampleMicro9feignMagazzinoInterface exampleMicro9feignMagazzinoInterface;
@GetMapping(value ="/magazzinoAPIEsterna/{codArt}")
public ResponseEntity<List<String>> magazzino(
@RequestHeader("Authorization") String authHeader,
@PathVariable("codArt") String codArt){
System.out.println("magazzinoAPIEsterna codArt=" + codArt);
return exampleMicro9feignMagazzinoInterface.magazzino(authHeader,codArt);
};

Questo ovviamente è un semplice esempio, forse troppo semplice per un lettore esperto, nel prossimo articolo verrà costruito il servizio prodotti proprio per usare concretamente il servizio di magazzino.

Uno dei casi d'uso classici e molto frequenti è dover chiamare un micro-servizio da un altro all'interno di un ciclo, nel nostro semplice esempio il servizio di elenco prodotti deve recuperare la quantità a magazzino per ogni prodotto. La soluzione più semplice e dinamica è creare un service che esegua la logica e il ciclo con la chiamata al Feign. Prima di tutto bisogna creare due classi Entity, la prima con i dati del prodotto, la seconda si può creare estensione della prima con solo in aggiunta il campo o i campi recurati dall'altro micro-servizio, in questo modo la prima classe sarà usata nel repository come immagine della base dati mentre la seconda viene usata come classe di ritorno della risorsa API del micro-servizio.

public class ExampleMicro9feignEntityWithMagazzino extends ExampleMicro9feignEntity{
public ExampleMicro9feignEntityWithMagazzino(ExampleMicro9feignEntity el){
this.set_id(el.get_id());
this.setNome(el.getNome());
this.magazzino="NULL";
}
private String magazzino;
}

Nel service poi basta recuperare l'elenco da una semplice classe Repository e poi ciclare ogi elemento per chiamare l'interfaccia del Feign che poi andrà in automatico a chiamare il microservizio esterno del magazzino:

@Service
@Transactional(readOnly = true)
public class ExampleMicro9feignService {
@Autowired
ExampleMicro9feignRepository repository;
@Autowired
ExampleMicro9feignMagazzinoInterface exampleMicro9feignMagazzinoInterface;
public List<ExampleMicro9feignEntityWithMagazzino> findAll(String authHeader){
List<ExampleMicro9feignEntity> l=repository.findAll();
List<ExampleMicro9feignEntityWithMagazzino> lm=
new ArrayList<ExampleMicro9feignEntityWithMagazzino>();
l.forEach( el -> lm.add(new ExampleMicro9feignEntityWithMagazzino(el) ) );
lm.forEach( el -> el.setMagazzino(
exampleMicro9feignMagazzinoInterface
.magazzino( authHeader, el.getNome()).getBody().get(0) ) );
return lm;
}
}

ovviamente questo esempio molto semplice non gestisce l'errore in caso di non disponibilità del servizio di magazzino chiamato, nel caso di errore si otterrebbe la exception:

java.net.ConnectException: Connessione rifiutata

La configurazione di default di Feign prevede dei timeout molto ampi, ma è possibile modificare questi parametri anche personalizzando i timeout a seconda del nome del servizio

feign.client.config.default.connect-timeout=20000
feign.client.config.default.read-timeout=20000
feign.client.config.NomeServizio.connect-timeout=1000
feign.client.config.NomeServizio.read-timeout=1000

Altri due parametri molto interessanti è la configurazione per gestire la compressione dei dati nel caso di transito di grandi quantità grazie a questa tecnica

feign.compression.request.enabled=true
feign.compression.response.enabled=true

Per l'elenco e la spiegazione dettagliata di ogni parametro di configurazione si rimanda alla documentazione ufficiale.

Il framework Spring e in particolcare Spring Boot per mette di creare un accentratore di informazione proprio nella filosofia del Cloud dove possono (e devono) essere immaginate quelle informazioni accentrabili, specialmente quelle immutabili. Questa tecnica non deve essere confusa con un servizio centrale all'interno di un ecosistema a micro-servizi, per esempio per un "elenco clienti" è indicata la creazione di un progetto mentre la secret del token JWT può essere salvata in un Cloud e poi richiamata da tutti i micro-servizi che ne devono fare uso. In un progetto di tipo Spring Boot Cloud è consigliato salvare tutte le informazioni non mutabiliti e centrali per il sistema comprese le chiavi e le password.

Per creare un microservizio di tipo Boot Cloud è indispensabile creare un progetto base vuoto e aggiungere le dipendenze nel file pom:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<type>pom</type>
<scope>import</scope>
<version>LATEST</version>
</dependency>

Le informazioni NON saranno salvate all'interno ma saranno salvate in un file di properties esterno gestito tramite GIT (locale o remoto) e recuperate in fase di avvio del microservizi di base.

Nel micriservizio bisogna dichiarare la annotation EnableConfigServer nella classe principale del microservizio:

import org.springframework.cloud.config.server.EnableConfigServer;
@EnableConfigServer
@SpringBootApplication
public class ExampleMicro10cloudConfigApplication { ... }

E configurare la cartella dove è salvato il file con le informazioni

server.port=8888
spring.application.name=ExampleMicro10cloudConfig
spring.cloud.config.server.git.uri=file:///home/alnao/.config/ExampleMicro10cloudConfig
spring.cloud.config.server.git.defaultLabel=master

In questo caso abbiamo definito un server nel quale cercare le configurazioni, con questa tecnica, l'architettura metterà a disposizione le api

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

dove application è il nome applicazione definito nel file di properties mentre profile è il nome che verrà passato dal client. Per esempio a noi basterà definire il file del tipo <fileserver>-<propfilo>.properties, nel nostro caso ExampleMicro10cloudConfig-configSicurezza.properties con all'interno

jwt.secret=alnaobello

A questo punto è necessario eseguire la commit git, se si usa un path locale basta lanciare questi semplici comandi

$ git init .
$ git add -A .
$ git commit -m "Add application.properties"

Dopo aver avviato il microservizio, si può procare al path server:porta/<nomeServer>/nomeProfilo
per esempio

http://localhost:8888/ExampleMicro10cloudConfig/configSicurezza

ed ottenere come risposta un json con all'interno il valore salvato nel file di properties in GIT.

{
...
"propertySources": [
{
"name": "....",
"source": {
"jwt.secret": "alnaobello"
}
}
]
}

E' possibile configurare un repository esterno e remoto come GitHub impostando le proprietà:

spring.cloud.config.server.git.uri= https://example.com/my/repo
spring.cloud.config.server.git.skipSslValidation: true

oppure anche in GitHUB per esempio

spring.cloud.config.server.git.uri= https://github.com/{application}

Altre configurazioni di GIT si possono trovare nella documentazione ufficiale del framework. L'esempio qua descritto può essere trovato al solito repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro10cloudConfig

Un microservizio che vuole recuperare valori da un servizio di Boot Cloud deve prima di tutto avere delle dipendente pom le due librerie base:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>LATEST</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>LATEST</version>
</dependency>

e aggiungere nel file di properties il riferimento al server e anche al profilo

spring.application.name=ExampleMicro09feign
spring.profiles.active=configSicurezza
spring.cloud.config.uri=http://localhost:8888

ma l'architettura impone che il file properties debba chiamarsi "bootstrap" e non "application", poca documentazione è disponibile a riguardo, in caso aggiornerò questo articolo. Il valore può essere recuperato dal Cloud in una classe component e configuration:

@Component
@ConfigurationProperties("examplemicro10cloudconfig")
public class ExampleMicro9feignCloudPropertiesConfig {
@Value("${jwt.secret}")
private String jwtSecret;

public String getJwtSecret() {
return jwtSecret;
}
public void setJwtSecret(String jwtSecret) {
this.jwtSecret = jwtSecret;
}
}

La classe java dove recupeare questi valori può eseguire l'injection della classe configuration e poi recuperare i valori con una semplice get

@Autowired
ExampleMicro9feignCloudPropertiesConfig config;
...
String secret=config.getJwtSecret();//recupero dal Cloud

Questo esempio mostra proprio il caso d'uso più semplice: il recupero di chiavi, password o altri valori immutabili che possono essere usati da più servizi in un ecosistema. Questa tecnica molto usata ha due ulteriori dettagli da approfondire: la gestione dei profili e lo strato di sicurezza delle chiamate che sarannno esposte in altro articolo. Questo semplice esempio può essere trovato al solito repository

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro09feign

un programmatore attento avrà notato una cosa curiosa: per eseguire questo esempio è indispensabile aver avviato contemporaneamente 3 micro-servizi

  • il gestore JWT che esegue l'autenticazione con il rilascio del token, senza questo non sarà possibile effettuarare la chiamate con un token valido
  • il cloud che ritorna ai chiamanti la secret per la gestione del jwt, questo è indispensabile ai client per validare il token
  • il micro-serivizio chiamato che riceve e valida il token

questo esempio è proprio un caso d'uso molto semplice ma frequente: non basta un micro-servizio per avviarsi ma è necessario che siano avviati e funzionanti tutti i componenti dell'ecosistema.

E' indispensabile creare uno stato di sicurezza nel Boot Cloud e in particolare quelle API che vengono esposte dal Boot Cloud e che forniscono informazioni importanti (o meno) devono essere sicurizzate. Ovviamente l'architettura mette a disposizione un modo molto semplice e efficace: la basic auth con username e password che il client deve usare e il server autentica. Per attivare questo tipo di autenticazione bisogna aggiungere la dipendenza della libreria security del framework nel pom.xml del server:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

E poi dichiare due properties con le credenziali

security.basic.enabled=false
sicurezza.user=alnao
sicurezza.pwd=fantastico

La classe di autenticazione è molto simile a quella già vista in un precedente articolo: si recuperano i valori delle credenziali e si definisce un metodo di configurazione della autenticazione

@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
public class ExampleMicro10cloudBasicSecurity extends WebSecurityConfigurerAdapter{
@Value("${sicurezza.user}")
String user;
@Value("${sicurezza.pwd}")
String pwd;
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
};
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
System.out.println(this.user + this.pwd);
auth.inMemoryAuthentication()
.withUser(this.user)
.password(new BCryptPasswordEncoder().encode(this.pwd))
.roles("USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http.csrf().disable().httpBasic()
// .and().authorizeRequests()
// .antMatchers("/**").hasAuthority("ROLE_USER")
;
}
}

Il micro-servizio chiamante che si collega a questo cloud ha bisogno di definire le credenziali nel file bootstrap.properties

spring.cloud.config.username=alnao
spring.cloud.config.password=fantastico

Senza questa definizione ci sarebbe un errore bloccante in fase di avvio del tipo

IllegalArgumentException: Could not resolve placeholder 'jwt.secret' in value "${jwt.secret}"

L'esempio completo di questo tipo può essere trovato al solito repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro10cloudConfig

In un moderno sistema informatico, le chiamate asincrone vengono gestite da un gestore di code dove una sorgente (detta producer) inserisce nella coda un elemento che rimane in coda finché qualcuno (detto consumer) non lo prende e lo elabora. Il concetto di producer-queue-consumer è molto diverso dal concetto di client server e bisogna sempre evitare di confondere i due concetti. Questo paradigma si presta benissimo in un sistema a microservizi scritto con Java Spring Boot dove ogni microservizio è indipendente e quindi decade proprio il concetto di client-server, in questo caso sia producer sia consumer saranno dei microservizi costruiti proprio partendo dal concetto di controller e service che già sono stati visti in tutti gli altri esempi.

Nel nostro caso la coda verrà gestita da un sistema RabbitMQ esterno ai micro-servizi ma può benissimo essere gestito da qualsiasi altro servizio simile come SQS di AWS. Per il prodotto MQ si rimanda ad altre guide e si consiglia uno studio approfondito prima di usare un prodotto così potente, nel nostro caso è stato creata una coda "ordiniMicroservizi" alla console di amministrazione disponibile all'indirizzo

http://127.0.0.1:15672/queues/ordiniMicroservizi

Nei due progetti creati con spring boot di tipo producer e di tipo consumer devono essere presenti la dipendenza ad AMQP:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

E deve essere creata una classe "bean" o "entity" che implementi la serializzazione, indispensabile quando dovremmo inserire un elemento di questo tipo nella coda in maniera serializzata e poi recuperata e gestirla dal consumer.

class ExampleMicro11asyncOrdineEntity implements Serializable {
private String userId;
private String idOrdine;
private Integer quantita;
... //getter & setter
}

In entrambe le applicazioni sarà necessario recuperare le configurazione del server/demone RabbitMQ, queste informazioni possono essere salvate nel SpringCloud come già fatto negli esempi precedenti. Primo passo è creare nel progetto Cloud il file ExampleMicro11async-configMQ.properties con le configurazioni della coda

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.queue=ordiniMicroservizi
spring.rabbitmq.exchange=amq.direct
spring.rabbitmq.routingkey=ordiniMicroservizi

Creare il file di configurazione del SpringCloud con il file Bootstrap.properties:

spring.application.name=ExampleMicro11async
spring.profiles.active=configMQ
spring.cloud.config.uri=http://localhost:8888
spring.cloud.config.username=alnao
spring.cloud.config.password=<fantastico>

e usare la annotation @EnableDiscoveryClient nelle classi Application che vogliono accedere al Spring Boot. Creare la classe di recupero dei dati dal Cloud

@Component
@ConfigurationProperties("examplemicro11async") //ExampleMicro11async
public class ExampleMicro11asyncProducerCloudConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private String port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.queue}")
private String queue;
@Value("${spring.rabbitmq.exchange}")
private String exchange;
@Value("${spring.rabbitmq.routingkey}")
private String routingkey;
...
}

Sarà poi compito dei microservizi usare queste configurazioni per caricare e leggere dati dalla coda (queue) di RabbitMQ. Conviene creare un progetto "Common" contenente il file bootstap.properties e la classe CloudConfig e poi importare la common negli altri due progetti nel pom.xml aggiungendo la dipendenza

<dependency>
<groupId>it.alnao.examples</groupId>
<artifactId>ExampleMicro11asyncCommon</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

Se viene usata questa tecnica, bisogna aggiungere ricordarsi di aggiungere una annotation particolare alle classi Application che usano la libreria Common

@PropertySources({ 
@PropertySource("classpath:bootstrap.properties"),
@PropertySource("classpath:application.properties")
})

In questo modo il micro-servizio caricherà il file bootstrap della libreria common e il file application, in questo secondo file sarà necessario definire solo la porta di avvio del servizio.

L'esempio di questa mini-libreria common è disponibile nel solito repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro11asyncCommon

Una guida completa a questa annotation può essere trovata nella guida ufficiale.

Il micro-servizio "producer" deve importare la libreria common e definire il file properties.application:

server.port=5053
ExampleMicro11asyncProducerController.message=Message has been sent Successfully.

Nella definizione di un service si può importare come autowired il template di RabbitMQ e la classe del Spring Cloud della libreria common:

@Service
public class ExampleMicro11asyncProducerService {
@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private ExampleMicro11asyncProducerCloudConfig config; //config del Spring Cloud

public void send(ExampleMicro11asyncOrdineEntity ordine){
rabbitTemplate.convertAndSend(config.getQueue(), ordine);
System.out.println("Send to queue "+ordine);
}
}

nel metodo send poi si invia alla coda l'oggetto ordine che, essendo serializzabile, non avrà problemi ad essere salvato ed essere successivamente letto. Nella classe Controller bisogna definire una api che riceve l'ordine e lo invia al service, a puro titolo di esempio si è aggiunta una properties per configurare un messaggio di risposta della API:

@RestController
@RequestMapping(value = "/api/sendOrder/")
public class ExampleMicro11asyncProducerController {
@Autowired
private ExampleMicro11asyncProducerService exampleMicro11asyncProducerService;

@Value("${ExampleMicro11asyncProducerController.message}")
private String message;

@PostMapping(value = "ordine")
public String publishUserDetails(@RequestBody ExampleMicro11asyncOrdineEntity user) {
exampleMicro11asyncProducerService.send(user);
return message;
}
}

Nella classe Application o esternamente in classi separate è necessario configurare la connessione al server RabbitMQ, in particolare è necessario definire un bean di tipo connectionFactory:

@Autowired
ExampleMicro11asyncProducerCloudConfig config; //config del Spring Cloud
@Bean
CachingConnectionFactory connectionFactory() {
CachingConnectionFactory cachingConnectionFactory =
new CachingConnectionFactory(config.getHost() );
cachingConnectionFactory.setUsername(config.getUsername() );
cachingConnectionFactory.setPassword(config.getPassword() );
return cachingConnectionFactory;
}

E due metodi Bean per convertire gli oggetti in json e definire il template rabbit usato nella classe service

@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
final RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jsonMessageConverter());
return rabbitTemplate;
}

Con questi componenti il micro-servizio è pronto per ricevere ordini e inviarsi alla coda MQ anche se il consumer non è attivo visto che sarà compito della coda poi gestire i messaggi. L'esempio completo è disponibile nel solito repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro11asyncProducerOrdini

Il micro-servizio consumer, finalizzato a ricevere i messaggi che arrivano dal producer, può essere una copia del producer e deve definire le proprietà base solo della porta che deve essere diversa dal producer se entrambi vengono eseguiti nello stesso sistema.  Il micro-servizio nella classe application deve importare tutte le proprietà già viste nella classe producer tanto che la classe può essere una copia o il codice può essere separato e posizionato nel progetto common.

A titolo di esempio è possibile gestire un sistema per salvare i dati in una tabella, come già visto nei precedenti progetti che salvano i dati con JPA in una tabella mongo, dynamo o di un database relazione, nel nostro esempio i prodotti vengono salvati in una tabella con entity, repository e service dedicati.

Per ricevere l'oggetto ordine dalla coda bisogna creare una classe Receiver dedicata di tipo component che importa il service con autowired e grazie alla annotation @RabbitListener riceve i messaggi dalla coda. All'interno del metodo basta eseguire il mapping tra classi e poi invocare il metodo del service per salvare i dati in tabella, in altri casi è possibile implementare qui la logica necessaria.

@Component
public class ExampleMicro11asyncConsumerReceiver
implements RabbitListenerConfigurer {

@Override
public void configureRabbitListeners(
RabbitListenerEndpointRegistrar rabbitListenerEndpointRegistrar) {
}
@Autowired
ExampleMicro11asyncConsumerService service;

@RabbitListener(queues = "${spring.rabbitmq.queue}" )
public void receivedMessage(ExampleMicro11asyncConsumerMagazzinoEntity ordine) {
System.out.println("Order: " + ordine);
ExampleMicro11asyncConsumerMagazzinoEntity el=
new ExampleMicro11asyncConsumerMagazzinoEntity();
el.setIdOrdine( ordine.getIdOrdine() );
el.setIdProdotto( ordine.getIdProdotto() );
el.setQuantita( ordine.getQuantita() );
service.save(el);
}
}

Questo semplice esempio è completamente asincrono e può essere testato in maniera completa: se uno dei due micro-servizi non è attivo l'altro può lavorare senza problemi e all'avvio del consumer tutti i messaggi in coda vengono elaborati e poi rimane in attesa di ricevere altri messaggi. L'esempio completo è disponibile nel solito repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/

L'orchestratore Eureka-Server" è una applicatione per la gestione delle informazioni di un ecosistema a micro-servizi, potremmo chiamarlo con la parola italiana "orchestratore". Per trasformare un microservizio in un Eureka server basta aggiungere la annotation alla classe applicazion principale:

@EnableEurekaServer
@SpringBootApplication()
@RestController
public class ExampleMicro12eurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleMicro12eurekaServerApplication.class, args);
}
@RequestMapping("/")
public String home() {
return "ExampleMicro12eurekaServerApplication";
}
}

E nel file pom del progetto bisogna aggiungere la libreria principale

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>3.1.1</version>
</dependency>

Nel file di properties bisogna ricordarsi di aggiungere anche i riferimenti per definire alcune informazioni tra cui l'url di registrazione (serviceUrl) e la path a cui il server esporrà le informazioni (dashboard.path)

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka
eureka.dashboard.path=/eurekawebui
eureka.instance.hostname=localhost

Una volta avviato il microservizio si può accedere alla console di Eureka con un browser:

http://localhost:8761/eurekawebui

I riferimenti sono la documentazione ufficiale e il repository ufficiale di Spring-Netflix visto che Eureka è stato un progetto studiato e finanziato dalla famosa casa americana e disponibile nel repository ufficiale. In aggiunta è da leggere come approfondimento anche un articolo nel blog ufficiale.

Il server Eureka è un monitor di servizi e i micro-servizi possono registrarsi se usano la libreria Actuator, le dipendenze delle librerie si devono aggiungere nel file pom:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

E poi necessario aggiungere l'url per iscriversi al server eureka e la configurazione del actuator con  le proprietà management.endpoints:

server.port=5062
spring.application.name=ClientMagazzinoApplication
eureka.client.serviceUrl.defaultZone=${EUREKA_URI:http://localhost:8761/eureka}
eureka.instance.preferIpAddress=true
management.endpoints.jmx.exposure.include=health,info
management.endpoints.web.exposure.include=health,info

Per l'attivazione del actuator è necessario aggiungere l'annotation nella classe application del micro-servizio:

@EnableDiscoveryClient
@SpringBootApplication
@RestController
public class ExampleMicro13actuatorApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleMicro13actuatorApplication.class, args);
}
@RequestMapping("/ExampleMicro13actuator")
public String exampleMicro13actuator() {
return "ExampleMicro13actuator";
}
}

Una volta avviato il microservizio si possono verificare le informazioni esposte dal componente actuator agli url con un browser o con un comando curl:

http://localhost:5062/actuator/health
http://localhost:5062/actuator/info

In caso di avvio del client senza il server Eureka si otteranno messaggi di warning del tipo

"Cannot execute request on any known server"
"Connessione rifiutata"

ma il micro-servizio sarà avviato correttamente e sarà impossibile registrarlo successivamente, se invece il server Eureka risulterà attivo prima che venga avviato un client, questo verrà registrato e sarà possibile notarlo nella console del server.

Anche in questo caso è possibile consultare e leggere la documentazione ufficiale e la documentazione del progetto Spring-Netflix.

Per ridondanza orizzontale si intende studiare un sistema di avviare più istanze dello stesso servizio per dividere il carico ed evitare brutte sorprese se uno si blocca per motivi tecnici. Il sistema per creare la ridondanza con Spring Boot è avviare lo stesso micro-servizio su porta diverse, per prima cosa bisogna indicare la porta nel file di properties in modo che sia dinamica, per esempio:

server.port=506${seq:1}

Da Eclipse si può configurare il parametro nella vista "Arguments" nel menù "config" del progetto dove bisogna aggiungere:

-Dseq=1

Per avviare più istanze è necessario creare il file jar e poi avviare il micro-servizio più volte

mvn install
mvn package
cd target
java -jar *.jar -Dseq=3
java -jar -Dseq=6 *.jar
java -jar -Dseq=7 *.jar

Bisogna ricordare che il parametro deve essere inserito prima del nome del jar altrimenti non funziona. In questo modo avviando più servizi, dal server Eureka si noteranno le istanze avviate su porte diverse anche se non c'è ancora il bilanciamento del carico che sarà esposto in futuri articoli. Nel file di properties si è utilizzata la tecnica di parametri di Spring con un valore di default con il due-punti in modo che in assenza del parametro specifico.

Per bilanciamento si intende la distribuzione del carico tra più istanze dello stesso micro-servizio, questo concetto si unisce alla ridondanza introdotta nel precedente articolo, in quanto per avere un sistema di bilanciamento di devono essere in esecuzione più istanze dello stesso micro-servizio. Il Ribbon è una libreria evoluzione del Eureka che permette di eseguire bilanciamenti lato client, cioè è il client che decide quale servizio chiamare in base alle informazioni in possesso.

Questa tecnica impone che il chiamante (client) debba essere scritto con spring boot, registrato in un server Eureka e che il cliente conosca sempre quante istanze del server siano attive e i rispettivi endpoint, questo permette di non aver un server "discovery" ma impone che il client conosca tutte le info prima di essere avviato. Come al solito la libreria necessita di essere censita nel pom.xml

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>

E la classe application del server deve avere la annotation RibbonClient con il nome del servizio

@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages=
{"com.netflix.client.config.IClientConfig"})
@RibbonClient(name="ExampleMicro14ribbon", configuration =
xampleMicro14ribbonConfiguration.class)
public class ExampleMicro14ribbonApplication {
@Value("${server.port}")
String port;
public static void main(String[] args) {
SpringApplication.run(ExampleMicro14ribbonApplication.class, args);
}
....
}

Per provarla bisogna sempre verificare che il server sia registrato

http://localhost:8761/eurekawebui

Per eseguire le prove del bilanciamento e della ridondanza bisogna avviare più istanze, per esempio con i comandi

$ mvn install
$ java -jar -Dseq=3 *.jar
$ java -jar -Dseq=4 *.jar

e nel server Eureka si possono vedere le istanze avviate, nel'esempio è stata creata anche una piccola risorsa di controllo che risponde al endpoint

http://localhost:5072/ExampleMicro14ribbon

Il client deve importare la stessa libreria e configurare nel file di proprietà i riferimenti ai server:

server.port=5080
spring.application.name=ExampleMicro14ribbonClient
eureka.instance.preferIpAddress=true
eureka.client.serviceUrl.defaultZone=${EUREKA_URI:http://localhost:8761/eureka}
eureka.client.registerWithEureka=true
eureka.client.fetchRegistry=true
eureka.client.healthcheck.enabled= true
eureka.instance.leaseRenewalIntervalInSeconds= 1
eureka.instance.leaseExpirationDurationInSeconds= 2
ribbon.eureka.enabled= false
ribbon.listOfServers = localhost:5071, localhost:5072

La classe application del client deve riportare sempre RibbonClient con lo stesso nome del server, in questo semplice esempio un metodo risorsa esegue la chiamata al server dove url viene recuperato dalla libreria LoadBalancerClient che recupera gli url dei server dalle configurazioni e esegue in automatico il bilanciamento del carico con la tectica "RoundRobin" cioè effettua in serie le chiamate (la prima al primo, la seconda chiamata al secondo micro-servizio e così via rincominciando dal primo quando i server sono numericamente finiti)

@RibbonClient( name = "ExampleMicro14ribbon", configuration = 
ExampleMicro14ribbonConfiguration.class) // For Ribbon
@EnableDiscoveryClient
@SpringBootApplication(scanBasePackages=
{"com.netflix.client.config.IClientConfig"})
@RestController
public class ExampleMicro14ribbonClientApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleMicro14ribbonClientApplication.class, args);
}
@RequestMapping("/ExampleMicro14ribbonClient")
public String home() {
String r="";
ServiceInstance serviceInstance=loadBalancer.choose("ExampleMicro14ribbon");
String baseUrl=serviceInstance.getUri().toString();
baseUrl=baseUrl+"/ExampleMicro14ribbon";
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response=null;
try{
response=restTemplate.exchange(baseUrl,
HttpMethod.GET, getHeaders(),String.class);
r=response.getBody() ;
}catch (Exception ex){
System.out.println(ex);
r="Error : " + ex.getMessage();
}
return "ExampleMicro14ribbonClientApplication from " + r ;
}
@Autowired
private LoadBalancerClient loadBalancer;
private static org.springframework.http.HttpEntity<?>
getHeaders() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
return new org.springframework.http.HttpEntity<>(headers);
}
}

Per provare basta controllare nel Eureka la corretta registerazione e poi chiamare il client

http://localhost:5080/ExampleMicro14ribbonClient

se nel server è indicato quale è la parta usata, si nota anche nel client cosa è stato chiamato. Questa tecnica impone che nel client siano configurati con valori fissi il numero e le porte dei server chiamati, limite molto rigido e non sempre tali informazioni sono disponibili, inoltre la ridondanza non è molto rispettata in quanto l'avvio di un server non viene visto in automatico dal client che necessita almeno di un riavvio per l'aggiornamento delle configurazioni.

Maggiori dettagli sono disponibili nella documentazione ufficiale ma trattandosi di bilanciamento client-side non è molto usata, spesso si preferisce la tecnica server-side come Zuul o un sistema esterno come il ALB di AWS.

Per bilanciamento si intende la distribuzione del carico tra più istanze dello stesso micro-servizio, questo concetto si unisce alla ridondanza introdotta nel precedente articolo, in quanto per avere un sistema di bilanciamento di devono essere in esecuzione più istanze dello stesso servizio. Zuul è una libreria che permette di creare un micro-servizio che esegua il bilanciamento leggendo le informazioni delle istanze avviate e disponibili dal Eureka, quindi in una infrastruttura c'è bisogno di un chiamate che invochi una risorsa su Zuul che legge da Eureka quali istanze del target sono avviate e disponibili. Come da documentazione ufficiale è necessario importare nel pom.xml le librerie

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>

La classe application deve solo usare le annotation @EnableZuulProxy e @EnableDiscoveryClient, la prima indica proprio che si tratta di un Zuul Server mentre la seconda indica che si tratta di un Eureka Client.

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class ExampleMicro15zuulApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleMicro15zuulApplication.class, args);
}
}

Le configurazioni importanti sono nel file di properties dove si deve censire la "rotta" cioè mappare un path ad un serviceId dove il path è l'endpoint esposto dallo Zuul mentre il serviceId è il nome con il quale i microservizi target si registrano nel Euraka, in questo modo il server Zuul effettuerà il "routing" (come un proxy) dal path al serviceId. In questo file sono censiti due path dei precedenti articoli con i microservizi relativi

#esempio ExampleMicro13actuator
zuul.routes.ExampleMicro13actuator.path = /ExampleMicro13actuator**
zuul.routes.ExampleMicro13actuator.serviceId=ExampleMicro13actuator
#esempio ExampleMicro14ribbon
zuul.routes.ExampleMicro14ribbon.path = /ExampleMicro14ribbon
zuul.routes.ExampleMicro14ribbon.serviceId=ExampleMicro14ribbon
#esempio con url alternativo se si tratta di urb fisso al posti di serviceId
#zuul.routes.ExampleMicro14ribbon.url = http://localhost:5071/
#proprietà ulteriori interessanti
ExampleMicro14ribbon.ribbon.readTimeOut= 60000
ExampleMicro14ribbon.ribbon.connectTimeOut= 20000
ExampleMicro14ribbon.ribbon.maxTotalHttpConnection= 500
ExampleMicro14ribbon.ribbon.maxConnectionsPerHosts= 100

Per eseguire la prova prima di tutto bisogna sempre verificare che tutti i microservizi siano avviati e attiva nel Eureka, senza questa registrazione l'ecosistema con Zuul non può funzionare.

http://localhost:8761/eurekawebui

I micro-servizi poi risponderanno agli endpoint censiti nel server Zuul che instraderà (con regole di routing/proxy) le chiamate verso i service/target configurati. In questo caso gli url sono:

http://localhost:5081/ExampleMicro13actuator
http://localhost:5081/ExampleMicro14ribbon

La documentazione ufficiale di questa libreria è veramente dettagliata e molto utile.

Per resilienza di un micro-servizio si intende la capacità di monitoraggio dei tempo di risposta per poter intervenire con la scalabilità verticale e/o orrizzontale. Il primo tool da usare è Hystrix che fa sempre parte del gruppo di cloud di Netflix, questo tool permette di interrompere un metodo se questo supera un certo timeout e eseguire al suo posto un altro metodo, questa tecnica viene può essere usata quando un service accedere ad una base dati molto lenta e si vuole intervenire in caso di tempi di risposta molto lunghi o di errore, fornendo una risposta standard oppure andando a recuperare la risposta in altro modo con un altro servizio. Per importare la libreria basta aggiungere nel file pom.xml:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>

Nel metodo da monitrare bisogna aggiungere la annotation dove si definisce il fallBackMethod richiamato in caso di errore

@HystrixCommand(fallbackMethod = "temperaturaGenericaSimple")
String getTemperaturaMediaSimple(Integer anno) {
if (anno > 2023)
throw new IllegalArgumentException("Anno futuro!");
//TODO: recupero temperatura media da un servizio
return "Alta";
}
String temperaturaGenericaSimple(Integer anno) {
System.out.println("ExampleMicro16hystrixService temperaturaGenerica");
return "Fa caldo a causa del riscaldamento globale";
}

In questo semplice esempio in caso di anno futuro, viene lanciata una exception che viene intercettata dal Hystrix e viene gestita con il metodo indicato. Questo semplice esempio può essere evoluto impostando un timeout, in modo che se il service non risponde in tempo possa essere eseguito un metodo alternativo, per questo la annotation di configurazione recupera il valore del timeout da una costante FailueTimeOutMs

//costante HYSTRIX per il timeout
public static final String FailueTimeOutMs = "4000";

@HystrixCommand(fallbackMethod="temperaturaGenerica",
threadPoolKey = "prim", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
value = FailueTimeOutMs)
}
)
String getTemperaturaMedia(Integer anno) throws InterruptedException {
if (anno > 2023)
throw new IllegalArgumentException("Anno futuro!");
int randomMS=ThreadLocalRandom.current().nextInt(2000,8000);//tra i 2 e 8 s
System.out.println("ExampleMicro16hystrixService wait time="+randomMS);
Thread.sleep(randomMS);
return "Normale";
}

In questo semplice esempio il random permette di simulare un servizio molto lento, provandolo si vede come alcune esecuzioni vanno lunghe e viene eseguito il metodo alternativo! Per il monitoraggio con actuator e successivamente Turbine bisogna ricordarsi di configurare le proprietà base della libreria:

management.endpoint.health.enabled=true 
management.endpoints.jmx.exposure.include=*
management.endpoints.web.exposure.include=*
management.endpoints.web.cors.allowed-origins=true
management.endpoint.health.show-details=always

e in particolare la risorsa esposta da Hystrix:

http://localhost:5082/actuator/hystrix.stream

permette di monitorare i tempi di risposta messi a disposizione dalla accoppiata actuator e hystrix, queste informazioni saranno poi usate dal Turbine.

L'esempio completo è a disposizione al solito repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro16hystrix

In una infrastruttura a micro-servizi costruita con Spring Cloud è fondamentale avere uno strumento per il monitoraggio di tutti i componenti del gruppo, questo per poter analizzare eventuali problemi e poter intervenire in tempo prima che il sistema crolli da problemi e errori magari di un servizio. Nel pacchetto sviluppato da Netflix per Spring Cloud, la libreria Turbine è studiata proprio per il monitoraggio di tutti i servizi che usano Hystrix. La libreria deve essere importata nel pom.xml

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>

e deve essere abilitata nelle annotation della clase Application:

@EnableHystrixDashboard
@EnableTurbine
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ExampleMicro17turbineApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleMicro17turbineApplication.class, args);
}
}

Nel file di configurazione poi bisogna censire tutte le proprietà necessarie di Turbine

hystrix.dashboard.proxy-stream-allow-list=localhost
turbine.appConfig=ExampleMicro17turbine
turbine.aggregator.clusterConfig=ExampleMicro17turbine
turbine.instanceInsertPort.crudrev= true
turbine.instanceMonitor.eventStream.skipLineLogic.enabled= false
turbine.ConfigPropertyBasedDiscovery.crudrev.instances=localhost
turbine.ConfigPropertyBasedDiscovery.crudrev.isSecure=false
turbine.ConfigPropertyBasedDiscovery.crudrev.instancePort=8091
turbine.clusterNameExpression= new String("default")

Una volta avviato il micro-servizio basterà andare nella pagina web

http://localhost:5083/turbine.stream?cluster=ExampleMicro17turbine

per poter controllare e monitorare l'andamento di tutti i servizi collegati, in questo caso si usa solo l'hystrix di se stesso ma è possibile "collegare" più servizi censendoli nella proprietà clusterConfig.

Il test-driven development (TDD) è una tecnica di sviluppo che prevede la scrittura dei test prima che venga scritto il codice, successivamente il codice scritto può considerarsi concluso solo quando tutti i test vengono eseguiti senza errori di compilazione. Il procedimento prevede i passi :

  1. analisi dei requisiti
  2. scrittura dei test
  3. esecuzione dei test con fallimento di tutti i test
  4. sviluppo del codice
  5. esecuzione dei test, in caso di qualche errore si ritorna al passo 4
  6. conclusione degli sviluppi

il TDD prevede questo ordine preciso con gli sviluppi che si possono considerare conclusi solo quando tutti i test sono eseguiti senza segnalazioni. Eventuali successive modifiche comportano la ripartenza dal primo punto e non un inizio parziale. I test sono tutti atomici e devono verificare una sola operazione del requisito e, all'interno di ogni test, devono essere sviluppati tre passi:

  1. Arrange: la definizione di tutti i dati di partenza
  2. Act: la chiamata alla operazione da testare (di solito è una sola operazione ma può essere anche multipla)
  3. Assert: la validazione del risultato confrontando la risposta del passo 2 con quanto definito nel punto 1

Per la creazione del progetto base (senza spring boot) è stato usato il comando maven standard

$ mvn archetype:generate

e per eseguire i test bisogna lanciare il comando

$ mvn test

L'esecuzione dei test in console può dare esito positivo con un messaggio del tipo

[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, 
Time elapsed: 0.033 s - in it.alnao.examples.AppTest

oppure un messaggio di errore in caso di test non passato con il messaggio

[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, 
Time elapsed: 0.004 s <<< FAILURE! - in it.alnao.examples.AppTest

e viene visualizzato quale test è fallito, per esempio impostando il valore atteso 2.4 compare il messaggio di errore

AppTest.dividiNormale:24 expected: <2.4> but was: <2.5>

E' possibile anche generare un report di maven e si usa il comando

$ mvn site

dopo però aver censito il report nel pom.xml

<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
</plugin>
</plugins>
</reporting>

e viene creato il sito con il report dei test nella cartella target nella sottocartella site.

Partendo da un requisito molto semplice come "un servizio che esegua la divisione di due numeri", si può pensare di eseguire questa mini analisi: prevedere la creazione di una metodo statico in Java che esegue la divisione di due numeri reali, il ritorno deve essere reale, per numeri reali si intende il tipo Double di java, bisogna prevedere anche il caso particolare: se il divisore è zero bisogna lanciare una exception java.

Da questa analisi si può pensare di scrivere due test: un primo metodo per verificare la divisione corretta, un secondo metodo per verificare che venga "lanciata" una exeption java in caso di dividendo zero. Ogni metodo deve avete la annotation Test e nel codice di esempio si descrivono i tre componenti Arrange, Act e Assert. 

@Test
public void dividiNormale(){
//1) Arrange: la definizione di tutti i dati di partenza
Double dividendo=new Double(5.0);
Double divisore=new Double(2.0);
Double resultAtteso=new Double(2.5);
//2) Act: la chiamata alla operazione da testare
Double result=App.dividi(dividendo, divisore);
//3) Assert: la risposta del passo 2 VS quanto definito nel punto 1
assertEquals(resultAtteso,result);
}
@Test
public void dividiPerZero(){
//1) Arrange: la definizione di tutti i dati di partenza
Double dividendo=new Double(5.0);
Double divisore=new Double(0);
//2) Act: la chiamata alla operazione da testare
try{
App.dividi(dividendo, divisore);
//3) Assert: la risposta del passo 2 VS quanto definito nel punto 1
assertTrue( false ); //errore senza exception
}catch (ArithmeticException e){
assertTrue( true ); //ok se ArithmeticException
}catch (Exception e){
assertTrue( false ); //errore se altre exception
}
}

Questo esempio è fatto con la versione 1.7 di Java e 4.13 della libreria di junit e può essere trovata al repository:

https://github.com/alnao/JavaSpringBootExample/tree/master/ExampleMicro18TDDJunit4

Una dei linguaggi più usato al mondo è Java ed è ovviamente possibile usare tale linguaggio per sviluppare funzioni AWS-Lambda per tutte le necessità. In questa serie di articoli saranno esposti alcuni esempi di funzioni per usi diversi ma avendo sempre come base il componente di accesso ai servizi AWS chiamato AmazonS3Client. In questo articolo sarà esposto il metodo base e "manuale" per creare funzioni lambda a titolo di esempio per mostrare nel dettaglio ogni singolo componente dell'architettura, in successivi articoli saranno esposti i metodi più automatici grazie ad AWS CLI, AWS SAM e AWS SDK che permettono al programmatore di saltare alcuni passaggi manuali che vengono eseguiti dai tool messi a disposizione dalla piattaforma. Il primo passo è creare un progetto Java con Maven, questo è possibile sia da Eclipse sia da MS Code, nel file pom.xml bisogna aggiungere alcune dipendenze necessarie per la compilazione:

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>

Oppure, nella procedura guidata di Eclipse, è possibile selezionare il tipo "aws-lambda-s3-example". Quello che bisogna creare è una classe e un metodo specifici che poi saranno il corpo della funzione lambda. La classe deve implementare RequestHandler che ha due archetipi: l'elenco scatenante in input e il tipo di output della funzione, infatti poi bisogna definire una funzione che i due tipi in ingresso e in uscita. Esempio:

public class LambdaRequestHandler implements RequestHandler<S3Event, String> {
@Override
public String handleRequest(S3Event s3event, Context context) {
System.out.println("Funzione lambda eseguita");
return "OK";
}
}

questa semplice classe è già pronta per essere compilata con il comando:

mvn pacakge

oppure lanciando la procedura guidata di ecplise mettendo "pacakge" come "goals", il comando va a creare un file jar all'interno della cartella target, questo pacchetto è pronto per esser rilaciato in AWS.
Nella console di AWS, nel servizio lambda si devono eseguire i seguenti passi:

  • creare manualmente una nuova funzione selezionando il nome e il tipo "Java 8 on Amazon Linux 1"
  • caricare il jar, selezionado il file dopo aver premuto la tendina "Upload from" nella vista code
  • impostare la classe nella sezione "Runtime settings" nella vista code, inserendo il valore nel tipo pacakge::metodo, per esempio "it.alnao.lambda.examples.Esample1::handleRequest"
  • impostare memoria e timeout nella scheda "General" nella vista "Configuration", di default i valori sono 512Mb e 15 secondi rispettivamente
  • impostare trigger e permission se necessario, in questo primo esempio non sono previsti

Per provare manualmente la funzione basta andare nella vista test e lanciare il bottone "test", il risultato sarà un messaggio "OK" e aprendo i dettagli si potrà vedere anche il messaggio scritto nella stream di output. Nella vista monitor sono disponibili i grafici di tutte le esecuzioni con il dettaglio di errori e un tab specifico per accedere ai log su CloudWatch.

L'esempio completo può essere trovato al git

https://github.com/alnao/AwsLambdaExamples/tree/master/java-maven-example01

E' possibile creare con CloudFormation un template per la creazione API Rest sviluppata in Java il servizio Lambda e il paradigma del server-less. Proma di tutto bisogna creare un template in formato yaml (o json) e inseire la definizione dei vari componenti, in questo semplice esempio sono due: la funzione e httpApi:

HelloWorldFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: HelloWorldFunction
Handler: helloworld.App::handleRequest
Runtime: java8
Environment:
Variables:
PARAM1: VALUE
Events:
HelloWorld:
Type: HttpApi
Properties:
ApiId: !Ref ES15HttpApi
Path: /hello
Method: GET
ES15HttpApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: !Ref StageName
RouteSettings:
"GET /hello":
ThrottlingBurstLimit: 500 # overridden in HttpApi

Per il codice in java bisogna creare un progetto java dentro ad una sottocartella "HelloWorldFunction" (valore del CodeUri) come un progetto maven ufficiale, con con file pom.xml dove bisogna indicare le dipendenze del progetto e le configurazioni del compilatore java, un esempio è disponibile:

https://github.com/alnao/AWSCloudFormationExamples/blob/master/Esempio15lambdaJava/HelloWorldFunction/pom.xml

Poi ovviamente bisogna creare la classe che verrà eseguita come Lambda, in questo caso bisogna creare un metodo java indicato nella proprietà Handler del template cloudformation: il package (helloworld), la classe (App) e il metodo (handleRequest), per esempio:

public class App implements RequestHandler<Object, Object> {
public Object handleRequest(final Object input, final Context context) {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-Custom-Header", "application/json");
try {
String output = String.format("{ \"message\": \"hello world\" }");
return new GatewayResponse(output, headers, 200);
} catch (Exception e) {
return new GatewayResponse("{}", headers, 500);
}
}
}

In questo semplice esempio è stata usata una classe POJO creata custom dal nome GatewayResponse

public class GatewayResponse {
private final String body;
private final Map<String, String> headers;
private final int statusCode;
...

Una volta compilato in locale con il comando

mvn install
mvn build

si possono eseguire i comandi aws-sam per eseguire il caricamento della funzione in un ambiente AWS come tutti gli altri template. In questo caso la classe java viene compilata e inserita dentro ad un pacchetto jar (anche se con estensione zip) e caricata nel bucket indicato in fase di deloy del comando sam. In alternativa a maven e al pom.xml è possibile usare il gradle creando il file build.gradle con le configurazioni del compilatore

Un esempio completo funzionante è disponibile sul GIT:

https://github.com/alnao/AWSCloudFormationExamples/tree/master/Esempio15lambdaJava

Grazie al plugin AWS di Eclipse è possibile creare funzioni lambda in Java con pochi click. Il plugin mette a disposizione alcuni tipi di progetto e in fase di creazione basta selezionare il tipo "Aws Lambda Java Project" indicando il pacakge java di destinazione; una volta creato il progetto sarà possibile eseguire il comando di compilazione:

mvn install
mvn build

visto che il progetto creato è un maven-standard può essere gestito e compilato come in normale progetto java e le relative dipendenze nel file pom.

Per eseguire il rilascio nel Cloud AWS bisogna selezionare la classe Handler (che di default è "LambdaFunctionHandler"), cliccando con il tasto destro del mouse compare la voce "Amazone web service" e la sottovoce "Upload function", nella procedura guidata di caricamento bisogna indicare le configurazioni base della Lambda:

  • il nome della lambda e il riferimento alla classe, è possibile sostituire una lambda esistente oppure crearne una nuova
  • la regola IAM, si può usare una regola pre-esistente o crearne una nuova direttamente nel tool
  • il bucket s3 bucket dove viene depositato il pachetto (jar/zip) del codice
  • le configurazioni avanzate di AWS-Lambda (per esempio la memoria e timeout)

Io non riesco ad usarlo in quanto, in fase di rilascio, si genera sempre un errore bloccante a causa della versione di JAXBException.

Grazie all'estensione AWS Toolkit per Visual studio Code è possibile creare e sviluppare velocemente Lambda con il linguaggio Java con pochi click e risulta anche molto più pratico e veloce del corrispondente per Eclipse. Dopo aver installato AWS Toolkit basta aprire il riquadro dei comandi (Ctrl+Maiusc+P) e selezionare la voce: 

> Create lambda SAM application

In questa piccola procedura guidata è possibile selezionare il tipo di linguaggio (java e maven), il modello (consigliato il base HelloWorld) e la cartella dove salvare il progetto.

Il risultato sarà un piccolo progetto con i componenti

  • cartella event con un file json di esempio che permetterà di eseguire i test da locale
  • il file template.yaml con il template completo di cloudformation con la definizione della lambda e la definizione della API tramite gateway
  • il progetto java-maven in una sotto-cartella con la classe App già pronta per essere eseguita

In questo piccolo progetto di esempio la lambda esegue una chiamata al servizio "checkip" per ritornare l'ip del chiamante. E' possibile eseguire localmente la lambda usando il comando CLI-SAM invocando la lambda e usando il file json di esempio come evento

$ sam local invoke HelloWorldFunction --event events/event.json

In questo caso il comando eseguirà sul docker locale la lambda e ritornerà il risultato. E' ovvio che docker deve essere installato e l'utente deve disporre delle autorizzazioni per eseguire i comandi docker.
In questo caso non funziona la funzionalità "upload lambda" disponibile cliccando con il tasto destro sul nome della cartella appena creata ma bisogna usare la CLI-SAM.

Il primo metodo per eseguire il rilascio è usare la riga di comando per compilare il progetto

$ sam build 

che crea una sotto-catella .aws-sam con dentro il compilato da maven pronto per essere rilasciato con il comando

$ sam deploy --guided

con il quale è possibile indicare ad AWS alcuni parametri come il nome del template su CloudFormation e poi viene salvato un file samconfig.toml con le configurazioni salvate. Una volta creato si può andare nella console web per vedere la lambda e il suo template CloudFormation, inoltre è possibile usare il toolkit per eseguire la lambda. Per rimuovere la lambda basta lanciare uno dei comandi:

aws cloudformation delete-stack --stack-name java-maven-example3
sam delete --stack-name java-maven-example3

Il secondo metodo per eseguire il rilascio è usare il toolkit: dal riquadro dei comandi bisogna selezionare la voce

> AWS deploy sam application

dove bisogna indicare il file yaml base, la region, il nome del bucket di appoggio e il nome del template. Nella output sarà possibile vedere lo stato avanzamento e l'esito finale del caricamento. Usando l'esensione del AWS Toolkit è possibile vedere l'elenco di tutte le funzionli Lambda create ed è possibile selezionare la voce "Invoke lambda" cliccando con il tasto destro del mouse in una lambda, nella schermata è possibile anche impostare i parametri di chiamata con la possiblità di selezionare il file json di esempio.

Per distruggere e rimuovere la lambda è possibile selezionare il template CloudFormation creato in fase di caricamento e selezionare l'opzione "Delete CloudFormation Stack", ovviamente tale operazione andrà a rimuovere tutte le risorse del template comprese altre se inserite manualmente quindi bisogna sempre prestare attenzione a quando si cancella uno stack in CloudFormation. La guida completa è disponibile nel sito ufficiale.

L'esempio completo funzionante può essere trovato nel GitHUB:

https://github.com/alnao/AwsLambdaExamples/tree/master/java-maven-example03-vscode

D'ora in avanti, per i successivi esempi verrà sempre usato questo Toolkit e non verrà usato Eclipse (visto che a me non funziona).

Tramite lambda è possibile creare una API per generare link con "signature", link che può essere da un client (browser) per scarire file da un bucket S3 che altrimenti sarebbe inaccessibile da internet. Per creare la lambda si usa la solita procedura da Eclipse o da VsCode, per esempio nel secondo caso

> Create lambda SAM application

nel progetto creato di default con la Hello di esempio bisogna prima di tutto aggiungere la libreria S3 al pom.xml:

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.340</version>
</dependency>

e poi bisogna ricordarsi di aggiungere la regola IAM perchè quella di default non prevede che la lambda acceda ad un bucket, per questo bisogna modificare il file template.yaml aggiungendo la regola di CloudFormation per la regola IAM, si rimanda agli articoli dedicati, nel dettaglio la regola deve avere la forma:

- Effect: Allow
Action:
- 's3:*'
Resource:
- 'arn:aws:s3:::nome-bucket/*'

Senza la regola IAM dedicata il link con signature sarà creato ma se usato il servizio S3 restituirà un errore "Access denied". Il codice java poi deve prevedere l'uso delle classi AmazonS3 e il client (tipo il boto3 di python)

APIGatewayProxyResponseEvent response = 
new APIGatewayProxyResponseEvent().withHeaders(new HashMap<>());
Regions clientRegion = Regions.EU_WEST_1;
AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
.withRegion(clientRegion)
.withCredentials(DefaultAWSCredentialsProviderChain.getInstance())
.build();

La impostazione della scadenza

java.util.Date expiration = new java.util.Date();
long expTimeMillis = Instant.now().toEpochMilli();
expTimeMillis += 1000 * 60 * 60 * 24;
expiration.setTime(expTimeMillis);

E poi si può creare il link con il metodo dedicato

URL url = s3Client.generatePresignedUrl(bucket, objectKey,expiration);
return response.withStatusCode(200).withBody(url.toURI().toString() );

Il codice completo di questo progetto di esempio può essere trovato al solito repository:

https://github.com/alnao/AwsLambdaExamples/tree/master/java-maven-example04-s3signature

Un esempio di sviluppo di Lamda con il linguaggio Java è scrivere una API che recuperi valori da una coda dal servizio SQS. Con la procedura guidata di VS Code si può creare un progetto HelloWord e poi bisogna ricordarsi di rinominare tutto il progetto, i package e i nomi da HelloWord nel nome desiderato, nel nostro esempio è stato usato Sqs. La libreria di default non contiene i package necessari, quindi nel pom.xml è necessario aggiungere la dipendenza alla libreria specifica di Sqs:

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>1.12.356</version>
</dependency>

Successivamente bisogna modificare il file yaml del template Cloud-Formation aggiungendo la regola IAM per permettere alla lambda di accedere alla coda, senza questa regola la lambda otterrebbe un brutto errore "access denied". La regola base che permette l'accesso a tutte le code è:

 - Effect: Allow
Action:
- 'sqs:*'
Resource:
- '*'

Nella classe e nel template creato di default è già disponibile una api con protocollo get, perfetto per un esempio di recupero di dati con una API-Rest. Nella classe java i passi semplici da eseguire sono: recuperare il riferimento alla coda (queueUrl) e poi recupeare i messaggi grazie alla libreria di AWS. In questo semplice esempio i messaggi della coda vengono concatenati in una stringa e poi ritornati come oggetto della riposta:

AmazonSQS sqs = AmazonSQSClientBuilder.defaultClient();
String queueUrl = sqs.getQueueUrl(QUEUE_NAME).getQueueUrl();
List<Message> messages = sqs.receiveMessage(queueUrl).getMessages();
//for (Message m : messages) {
// sqs.deleteMessage(queueUrl, m.getReceiptHandle());
//}
String messagess="";
for (Message m : messages) {
messagess+=m.toString();
}
return response.withStatusCode(200).withBody(messagess);

L'esempio completo e funzionante è disponibile nel solito repository:

https://github.com/alnao/AwsLambdaExamples/tree/master/java-maven-example05-sqs

La guida completa a tutte le funzioni previste dalla libreria ufficiale è disponibile nella documentazione ufficiale.

Uno dei casi d'uso più frequesti dell'uso di lambda in Java è l'uso delle librerie comuni per eseguire operazioni su oggetti o file in AWS, un semplice ma completo esempio è la costruzione di una lambda per convertire un file Excel in CSV. Partendo dal progetto base creato da Visual Studio Code si devono sviluupare un po' di modifica:

  • modificare il pacakge Java di default con un nome più consono, per esempio exc2csv
  • nel pom.xml oltre alle librerie base AWS si possono aggiungere le librerie necessarie, per esempio :
<dependency> 
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.11</version>
</dependency>
  • aggiungere nel template.yaml le regole IAM per permettere alla Lambda di accedere al Bucket e la definizione del trigger EventBridge, in modo che la lambda venga invocata ogni volta che viene caricato un file nel bucket nel path specificato, per questo si può vedere l'esempio nel repository o leggere la sezione dedicata su CloudFormation.
  • nella classe java bisogna prima recuperare i dati da EventBridge e poi suare la libreria per convertire il file.

I componenti Java usati sono:

  • client S3
private AmazonS3 s3Client = AmazonS3ClientBuilder.standard().build();
  • recupero dell'evento eventBridge (da verificare)
java.util.LinkedHashMap m1=(java.util.LinkedHashMap) event;
java.util.LinkedHashMap m3=(java.util.LinkedHashMap) m1.get("detail");
java.util.LinkedHashMap m4=(java.util.LinkedHashMap) m3.get("bucket");
java.util.LinkedHashMap m5=(java.util.LinkedHashMap) m3.get("object");
String bucket = (String) m4.get("name");
String key = (String) m5.get("key");
  • lettura del file
S3Object response = s3Client.getObject(bucket, key);
InputStream in = response.getObjectContent();
Workbook workbook = null;
workbook = new XSSFWorkbook(in);
  • conversione del file con la libreria "org.apache.poi"
  • scrittura del file CSV
ByteArrayInputStream inputStream = 
new ByteArrayInputStream(data.toString().getBytes());
String destFile=key.replace(".xlsx",".csv").replace(".xls",".csv");
s3Client.putObject(bucket, destFile, inputStream, new ObjectMetadata());
context.getLogger().log("destFile ");
if (workbook!=null){
workbook.close();
}
return destFile;

L'esempio completo può essere trovato al solito repository

https://github.com/alnao/AwsLambdaExamples/tree/master/java-maven-example06-excel2csv

Il pacchetto SDK di AWS mette a disposizione un insieme di strumenti con le quali è possibile creare codice e applicazioni che comunicano e sfruttano con i sottoservizi AWS usando le API. Utilizzando il linguaggio Java si deve usare Apache Maven per la gestione delle dipendente delle librerie e della compilazione. Per usare questo SDK, oltre all'ambiente di sviluppo con Java e Maven è necessario configurare un profilo programmatico AWS e la AWS CLI come indicato nella documentazione ufficiale e spiegato nell'articolo "CLI Command line interface".

Per la creazione del progetto è possibile usare il comando mvn visto che il SDK mette a disposizione un archetype:

mvn archetype:generate 
-DarchetypeGroupId=software.amazon.awssdk
-DarchetypeArtifactId=archetype-app-quickstart
-DarchetypeVersion=2.18.16

comando che necessita l'inserimento delle informazioni base del progetto, per esempio:

javaSdkVersion: 2.18.16
service: service
httpClient: apache-client
nativeImage: false
groupId: ALNAO
artifactId: 001CreateBucket
version: 1.0-SNAPSHOT
package: it.alnao.awssdkexamples

Da notare che, trattandosi di progetto maven standard, sono presenti anche le classi di test, visto che è possibile creare unit test specifiche.

La documentazione ufficiale è molto fornita di esempi che possono essere seguiti ed analizzati.

Inoltre è disponibile anche un repository pubblico ufficiale dove sono disponibili esempi di tutti i tipi:

https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javav2

Un semplicissimo ma utile esempio di sviluppo con la piattaforma SDK di AWS in Java è la realizzazione di un piccolo programma per la creazione ed la distruzione di un bucket S3. Questo semplice esempio è stato costruito studiando la documentazione ufficiale  e gli esempi presenti nel repository ufficiale.

In questo esempio vengono definiti tre metodi per la creazione, la canncellazione e la verifica dell'esistenza di un bucket S3. Si basano sul client S3 del SDK che deve essere creata con i comandi:

ProfileCredentialsProvider credentialsProvider = 
ProfileCredentialsProvider.create();
Region region = Region.EU_WEST_1;
S3Client s3 = S3Client.builder().region(region)
.credentialsProvider(credentialsProvider).build();

Poi un semplice metodo per la creazione di un bucket è

public static void createBucket( S3Client s3Client, String bucketName) {
try {
CreateBucketRequest bucketRequest = CreateBucketRequest.builder()
.bucket(bucketName).build();
s3Client.createBucket(bucketRequest);
// Wait until the bucket is created and print out the response.
HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder()
.bucket(bucketName).build();
S3Waiter s3Waiter = s3Client.waiter();
WaiterResponse<HeadBucketResponse> waiterResponse = s3Waiter
.waitUntilBucketExists(bucketRequestWait);
//waiterResponse.matched().response().ifPresent(System.out::println);
logger.info(bucketName +" is ready");
} catch (S3Exception e) {
System.err.println("Errore: " + e.awsErrorDetails().errorMessage());
return;
}
}

La relativa test-unit definisce un semplice metodo che crea il bucket e ne verifica l'esistenza con un secondo metodo

@Test
@Order(2)
public void createBucket() {
System.out.println("Test 2 createBucket started");
App.createBucket(s3,bucketName);
boolean result=App.checkIfBucketExist(s3,bucketName);
assertTrue(result);
System.out.println("Test 2 createBucket passed");
}

Il metodo dell'esistenza del bucket può essere sempre definito nella classe principale, basta infatti andare a scaricare l'elenco dei bucket esistenti e fare una ricarca del nome

public static boolean checkIfBucketExist( S3Client s3Client, String bucketName) {
logger.info(bucketName +" checkIfBucketExist");
ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest);
boolean exist=false;
Iterator<Bucket> it=listBucketsResponse.buckets().iterator();
while(it.hasNext()) {
Bucket b=it.next();
if (b.name().equals(bucketName))
exist=true;
}
logger.info(bucketName +" checkIfBucketExist " + (exist ? "YES" : "NO" ) );
return exist;
}

Questo esempio completo e funzionante è disponibile al repository

https://github.com/alnao/JavaExamples/tree/master/AwsSdkJava/01_CreateBucket

Per Java Gnome GTK (detto anche Java Gnome) si intendono quelle librerie Java sviluppate da java-gnome.sourceforge.net che creano un'interfaccia tra Java e le libreria GTK+, questo permette ad un programmatore Java di sviluppare applicazioni grafiche utilizzando le libreria GTK+ senza usare le più famose librerie AWT e SWING.

Tale accoppiata permette di sviluppare applicazioni solide, fruttando le grandi potenzialità e librerie di Java unite alla potenza delle librerie GTK senza dover utilizzare il linguaggio C/C++ che risulta oggi troppo complicato. Ovviamente sviluppare applicazioni grafiche con Java Gnome GTK ha una limitazione: le applicazioni funzioneranno solamente su ambienti dove sia installata un JVM (Java Virtual Machine) e dove siano installate e funzionanti le librerie GTK, ma questo non dovrebbe essere un problema visto che è proprio la filosofia di Java e JVM scrivere programmi cross-platform, per quanto riguarda le librerie GTK oltre ai sistemi GNU Linux, potete utilizzarli su tutti i sistemi Unix compreso anche MacOS e anche su Microsoft Windows (dalla versione 2000).

Se utilizzate Debian (o una sua derivata) l'installazione e la configurazione di un ambiente di sviluppo è semplice e veloce, infatti trovate già il pacchetto "libjava-gnome-java" e vi basta installarlo da Synaptic con pochi click. Come ambiente di sviluppo consiglio Eclipse, però consiglio di non utilizzare la versione disponibile sui repository ufficiali Debian ma consiglio di scaricarlo dal sito ufficiale www.eclipse.org, questo perché non necessità di installazione e si auto-aggiorna da solo. Una volta creato un progetto Java "normale", bisogna aggiungere libreria JavaGTK, tale libreria si trova al path

\usr\share\java\gtk-4.1.jar

e la si aggiunge cliccando sull'opzione "add external jar" dal classPath del progetto. Fatto questo si può già creare la prima classe con le librerie, per esempio:

package it.alnao.gtk.example;
import org.gnome.gdk.Event;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.Widget;
import org.gnome.gtk.Window;
import org.gnome.gtk.WindowPosition;
/**
 * AlNao Java Gnome GTK HandBook
 * @author AlNao (www.alnao.it)
 * ispired ZetCode Java Gnome tutorial (jan bodnar - website zetcode.com)
 * ispired official site java-gnome.sourceforge.net
 */
public class GdkSimple extends Window {
  public GdkSimple() {
    setTitle("Example");
    connect(new Window.DeleteEvent() {
      public boolean onDeleteEvent(Widget source, Event event) {
        Gtk.mainQuit();
        return false;
      }
    });
    setDefaultSize(250, 150);
    setPosition(WindowPosition.CENTER);
    show();
  }
  public static void main(String[] args) {
    Gtk.init(args);
    new GdkSimple(); 
    Gtk.main(); 
  } 
}

provando ad eseguire tale codice comparirà una finestra senza nulla con il titolo "Example" tipo questa:

GtkExample

GtkExample

Nel precedente articolo abbiamo visto come compilare il primo semplice programma che ora andremo a vedere nel dettaglio analizzando quasi tutte le righe di codice scritte. Prima di tutto ci sono gli import delle classi usate: tali componenti e tutte le altri classi del package org.gnome.gdk sono dentro al jar aggiunto nel classpath nell'articolo precedente:

import org.gnome.gdk.Event;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.Widget;
import org.gnome.gtk.Window;
import org.gnome.gtk.WindowPosition;

La nostra classe estende il tipo Window (org.gnome.gtk.Window) che è il livello principale che viene usato per contenere tutti gli altri oggetti (chiamati anche widgets)

public class GtkSimple extends Window {

Prima chiamata ad un metodo ereditato dalla classe Window, questo metodo serve per dare un titolo alla finestra

setTitle("Example");

il metodo connect ereditato dalla classe Windows, in questo caso serve per definire un nuovo evento "DeleteEvent" alla quale viene associata un sottometodo che esegue l'istruzione "Gtk.mainQuit()", cioè alla chiusura della finestra eseguire il quit del programma: alla chiusura delle finestra viene chiuso anche il processo (senza questa istruzione alla chiusura della finestra il processo rimane attivo nella cartella /proc).

connect(new Window.DeleteEvent() {
 public boolean onDeleteEvent(Widget source, Event event) {
  Gtk.mainQuit();
  return false;
 }
});

Altri metodi ereditati servono per definire la grandezza della finestra e la posizione della finestra rispetto al desktop

setDefaultSize(250, 150);
setPosition(WindowPosition.CENTER);

Metodo importantissimo serve per visualizzare la finestra una volta impostati tutte le proprietà grafiche (senza questa istruzione il processo parte senza mostrare la finestra che stiamo definendo)

show();

Metodo main della classe java che serve per fare l'init GTK e poi lanciare il costruttore della classe che esegue tutte le istruzioni viste sopra fino al metodo show();

public static void main(String[] args) {
 Gtk.init(args);
 new GtkSimple ();
 Gtk.main();
}

Il risultato finale di questa semplice classe sarà:

GtkExample

GtkExample

La prima classe vista nei precedenti articoli è la classe Window, che serve proprio per disegnare la "finestra" ed il principale widget che poi contiene tutti gli altri componenti del programma come bottoni, menu, label. Tipicamente conviene sempre eseguire lo hide appena dopo il costruttore per poi fare uno show alla fine delle valorizzazioni delle proprietà della finestra e l'aggiunta dei widget per evitare che a video si veda la finestra vuota che si compone dei componenti, per esempio:

 w = new Window();
 w.hide();
 // chiamata al metodo add per aggiungere i widget nella finestra
 w.showAll();

questa classe poi permette di aggiungere e definire una serie di proprietà della stessa finestra come la dimensione, posizione, icona, colori. All'interno della classe Window bisogna sempre ricordarsi di definire l'evento che termina in programma quando la finestra principale viene chiusa, questo viene fatto grazie a questo semplice codice:

connect(new Window.DeleteEvent() {
 public boolean onDeleteEvent(Widget source, Event event) {
  Gtk.mainQuit();
  return false;
 }
});

Consiglio come prima cosa di creare una classe abstract per definire le basi della finestra

package it.alnao.mygtk.example;
import org.gnome.gdk.Event;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.Widget;
import org.gnome.gtk.Window;
/** AlNao Java Gnome GTK Handbook
 * @author AlNao (www.alnao.it)
 */
public abstract class AMyGtkWindow{
 private Window mainWin;
 
 public void run(String[] args, String title, int width, int height, int border) {
  Gtk.init(args);
  mainWin = new Window();
  mainWin.connect(new Window.DeleteEvent() {
   public boolean onDeleteEvent(Widget source, Event event) {
    Gtk.mainQuit();
    return false;
   }
  });
  mainWin.setTitle(title);
  mainWin.setDefaultSize(width, height);
  mainWin.setBorderWidth(border);
  setupExample();
  mainWin.showAll();
  Gtk.main();
 }
 public void addWidget(Widget child){
  mainWin.add(child);
 }
 public abstract void setupExample();
}

E poi basta fare una classe per avere una finestra vuota:

package it.alnao.mygtk.example;
public class MyWindowExample extends AMyGtkWindow {
 public static void main(String[] args) {
  new MyWindowExample().run(args, "Window Example", 250, 100, 10);
 }
 @Override
 public void setupExample() {
  //empty
 }
}

Per maggiori informazioni sulla classe Window potete guardare il javadoc oppure la documentazione ufficiale alla pagina Window.

Il primo componente utile per le varie applicazioni è ovviamente il comune bottone che poi andremo a personalizzare, questo componente può prevedere un evento al quale poi si associa un metodo da lanciare, in questo caso si può parlare di programmazione ad eventi e non più programmazione ad oggetti semplice. Per aggiungere un bottone ad una finestra basta definire un oggetto di tipo button per esempio:

public class MyButtonExample extends AmyGtkWindow {
 public static void main(String[] args) {
  new MyButtonExample().run(args, "Button Example", 250, 100, 10);
 }
 @Override
 public void setupExample() {
  Fixed fix = new Fixed();
  Button btn1 = new Button("Button");
  btn1.setSensitive(false);
  Button btn2 = new Button("Button");
  Button btn3 = new Button(Stock.CLOSE);
  Button btn4 = new Button("Button");
  btn4.setSizeRequest(80, 40);
  fix.put(btn1, 20, 30);
  fix.put(btn2, 100, 30);
  fix.put(btn3, 20, 80);
  fix.put(btn4, 100, 80);
  addWidget(fix);
 }
}

In questo caso abbiamo creato quattro bottoni un un pannello della nostra Window, il primo è disattivato dall'istruzione setSensitive(false), il secondo ha solo la scritta "Button", il terzo è di stile "chiudi" con anche l'icona mentre il quarto è di dimensioni diverse, notare che nessuno di questi bottoni non fa nulla perché non abbiamo associato nessun evento. Per rendere operativo un bottone con un evento basta inserire nel codice (del metodo setupExample) le righe di codice:

 btn3.connect(new Button.Clicked() {
  public void onClicked(Button button) {
   Gtk.mainQuit();
  }
 });

dove associamo all'oggetto btn3 all'evento onClicked le istruzioni indicate nel metodo, in questo caso "mainQuit" cioè la chiusura dell'applicazioni. Il risultato è questa piccola finestra con i bottoni e con il bottone "Close" che effettivamente chiude l'applicazione.

buttons

Per maggiori informazioni sulla classe Button potete guardare il javadoc oppure la documentazione ufficiale alla pagina button.

In questo semplice e veloce esempio, vedremo come aggiungere un'icona alla finestra (della classe Window) nella parte sinistra in alto sulla barra del titolo. Per aggiungere l'icona alla classe basta creare un oggetto ti tipo Pixbuf passando al costruttore il path dell'immagine che vogliamo valorizzare come icona e poi lanciare il metodo setIcon passando come parametro l'oggetto appena creato
try {
 icon = new Pixbuf("web.png");
} catch (FileNotFoundException e) {
 e.printStackTrace();
}
setIcon(icon);
Per maggiori dettagli sulla clase Pixbuf potete vedere il javadoc oppure la documentazione ufficiale della classe Pixbuf.

Quando si progetta una l'interfaccia di un programma (GUI) bisogna sempre studiare il layout finale valutando le posizioni degli oggetti presenti nelle nostre finestre, i widgets a disposizione vengono chiamati layout containers, nei prossimi articoli vedremo le classi per definire posizioni fisse, allineamenti, box e tabelle. Il primo componente che bisogna vedere è il tipo fixed che è un contenitore per il posizionamento e il disegno di oggeti fisso di dimensioni fisse, questo è molto limitato come componente in quanto non permette assolutamente la gestione automatica delle dimensioni e del layout quindi nella maggior parte delle applicazioni non viene mai usato questo componente per la sua poca flessibilità ma ci sono alcune finestre che lo possono osare.

Fixed fix = new Fixed();
private Pixbuf rotunda;
pixbufAlNao = new Pixbuf("alnaoIT.jpg");
Image imageAlNao = new Image(pixbufAlNao);
fix.put(imageAlNao, 20, 30);
window.add(fix);

Nel nostro esempio abbiamo fatto una semplice finestra con una immagine all'interno posizionata in una posizione ben definita della finestra, quindi anche ridimensionandola e spostandola sarà sempre la stessa. Per prima cosa abbiamo definito l'oggetto di tipo Fixed, poi abbiamo creato l'immagine tramite Pixbuf e Image per recupero dell'immagine da disco e poi abbiamo aggiunto la nostra immagine al nostro oggetto Fixed indicando chiaramente la posizione in pixel di distanza dal punto in alto a sinistra (la distanza dagli assi x=20 e y=30), per poi agganciare il nostro oggetto alla finestra per il disegno finale.

Il risultato finale sarà una finestra con la immagine nelle posizioni fisse anche se proviamo a modificare la dimensione della finestra:

javagnomefixed

Se vogliamo inserire degli oggetti nelle nostre finestre posizionati con un allineamento particolare dobbiamo usare la classe Alignment che ci permette di posizionare qualsiasi oggetto nella finestra con un allineamento prefissato in modo che questo si sposti automaticamente al ridimensionamento della finestra, per esempio posizioniamo due bottoni allineati a sinistra, grazie alla classe Alignment questi bottoni si sposteranno se si ridimensiona la finestra rimanendo sempre posizionati nella parte sinistra della finestra.

 VBox vbox = new VBox(false, 5);
 HBox hbox = new HBox(true, 3);
 vbox.packStart(new Label(""));
 Button ok = new Button("OK");
 ok.setSizeRequest(70, 30);
 Button close = new Button("Close");
 hbox.add(ok);
 hbox.add(close);
 Alignment halign = new Alignment(1, 0, 0, 0);
 halign.add(hbox);
 vbox.packStart(halign, false, false, 3);
 add(vbox);

Nel codice vengono creati due box (uno verticale e uno orizzontale) e grazie al costruttore di Alignment i bottoni vengono allineati alla destra. Il risultato finale sarà una finestra come questa alignment

Per posizionare oggetti in una finestra simulando una griglia o una tabella bisogna usare la classe Table che ci permette di creare strutture dove posizionare gli oggetti come vogliamo e dove vogliamo, bisogna sempre tener conto che usando la classe table, gli oggetti saranno gestiti tramite riga/colonna. Per esempio creiamo una tabella per una calcolatrice usando la classe Table:

 Table table = new Table(5, 4, true);
 table.attach(new Button("Canc"), 0, 1, 0, 1);
 table.attach(new Button("Back"), 1, 2, 0, 1);
 table.attach(new Label(""), 2, 3, 0, 1);
 table.attach(new Button("Chiudi"), 3, 4, 0, 1);
 table.attach(new Button("7"), 0, 1, 1, 2);
 table.attach(new Button("8"), 1, 2, 1, 2);
 table.attach(new Button("9"), 2, 3, 1, 2);
 table.attach(new Button("/"), 3, 4, 1, 2);
 table.attach(new Button("4"), 0, 1, 2, 3);
 table.attach(new Button("5"), 1, 2, 2, 3);
 table.attach(new Button("6"), 2, 3, 2, 3);
 table.attach(new Button("*"), 3, 4, 2, 3);
 table.attach(new Button("1"), 0, 1, 3, 4);
 table.attach(new Button("2"), 1, 2, 3, 4);
 table.attach(new Button("3"), 2, 3, 3, 4);
 table.attach(new Button("-"), 3, 4, 3, 4);
 table.attach(new Button("0"), 0, 1, 4, 5);
 table.attach(new Button("."), 1, 2, 4, 5);
 table.attach(new Button("="), 2, 3, 4, 5);
 table.attach(new Button("+"), 3, 4, 4, 5);
 VBox vbox = new VBox(false, 2);
 vbox.packStart(new Entry(), false, false, 0);
 vbox.packStart(table, true, true, 0);

In poche parole con il costruttore abbiamo definito la struttura della tabella (cioè abbiamo indicato in numero di righe e il numero di colonne)

Table table = new Table(5, 4, true);

Il terzo parametro indica invece se la tabella deve essere omogenea (cioè se le righe devono avere la stessa altezza e le colonne la stessa larghezza). Poi aggiungiamo un bottone ad una determinata posizione,

table.attach(new Button("Canc"), 0, 1, 0, 1);

il secondo e il terzo parametro indicano la posizione da sinistra a destra mentre il quarto e il quinto parametro sono la posizione dall'alto verso sinistra. Con le ultime istruzioni si crea un VBox per la visualizzazione della tabella. Il risultato sarà una calcolatrice con i bottoni che non fanno nulla:

calculator

Per gli allineamenti di elementi grafici all'interno di una finestra si può usare il metodo PackStar della classe HBox, per esempio:

 VBox vbox = new VBox(false, 5);
 HBox hbox1 = new HBox(false, 0);
 HBox hbox2 = new HBox(false, 0);
 HBox hbox3 = new HBox(false, 0);
 Button button1 = new Button("Button");
 Button button2 = new Button("Button");
 Button button3 = new Button("Button");
 hbox1.packStart(button1, false, false, 0);
 hbox2.packStart(button2, true, false, 0);
 hbox3.packStart(button3, true, true, 0);
 vbox.packStart(hbox1, false, false, 0);
 vbox.packStart(hbox2, false, false, 0);
 vbox.packStart(hbox3, false, false, 0);
 add(vbox);

In questo caso abbiamo aggiunto tre bottoni ma con tre combinazioni diverse di parametri nel metodo packStart, il primo parametro indica l’oggetto (widget) da inserire nel box, il secondo indica se l’oggetto deve essere espanso (expand), il terzo indica se l’oggetto deve riempire tutto lo spazio disponibile (fill) mentre il quarto parametro sono i pixel di padding. Nell'esempio, nel primo caso non abbiamo indicato nessun allineamento quindi il bottone verrà posizionato come semplice bottone allineato a sinistra senza essere espanso, nel secondo bottone il box verrà espanso (quindi prenderà tutto lo spazio orizzontale) però il bottone non riempirà tutto lo spazio del box visto che il secondo parametro è false quindi il risultato sarà che il bottone sarà centrato ma di dimensioni ridotte; nel terzo caso invece il bottone verrà espanso e riempirà tutto lo spazio risultando più largo dei precedenti bottoni.

Il risultato finale sarà:

boxes

Vediamo un primo esempio semplice con gli allineamenti

 VBox vbox = new VBox(false, 10);
 Label label = new Label("Name:");
 Entry entry = new Entry();
 HBox hbox1 = new HBox(false, 5);
 hbox1.packStart(label, false, false, 0);
 hbox1.packStart(entry, true, true, 0);
 vbox.packStart(hbox1, false, false, 0);
 TextView tw = new TextView();
 vbox.packStart(tw);
 HBox hbox2 = new HBox(true, 5);
 Button ok = new Button("OK");
 Button close = new Button("Close");
 close.setSizeRequest(65, 30);
 hbox2.packStart(ok);
 hbox2.packStart(close);
 Alignment halign = new Alignment(1, 0, 0, 0);
 halign.add(hbox2);
 vbox.packStart(halign, false, false, 0);
 add(vbox);
 setBorderWidth(10);

In questa porzione di codice abbiamo creato una finestra con alcuni allineamenti, dove le istruzioni chiave sono le chiamate ai metodi packStart dei vari box, infatti bisogna pensare alla finestra disegnata come in insieme di box verticali e orizzontali dove noi andiamo a posizionare gli elementi secondo i vari parametri, per esempio l’oggetto label è stato posizionato con i parametri false, false quindi non viene “espanso” quindi riempirà il minor spazio possibile mentre l’oggetto entry che viene posizionato con i parametri true,true viene espanso e copre tutto lo spazio rimanente nel box verticale hbox1.

Il risultato finale sarà una finestra così disegnata:

La barra dei menù è una delle parti comuni e importanti di una qualsiasi applicazione desktop GUI dove si possono raggruppare tutti i comandi in maniera ordinata e gli utenti sanno benissimo dove andare a cercare le funzioni quando servono soprattutto se il menù è fatto bene e se è ordinato anche in maniera logica. Vediamo un primo esempio di menù dove metteremo il comando per “Quit” per uscire dal programma, usiamo prima la classe MenuBar che definisce tutta la barra orizzontale e poi la classe MenuItem che definisce la voce del menù principale e poi il attraverso Menu e MenuItem si definisce la voce “quit” a cui colleghiamo l’evento “activate” con un metodo che permette di definire cosa fare alla selezione della voce, nel nostro caso c’è la chiamata a “mainQuit” che chiude l’applicazione. Alla fine associamo tutto ad un box che poi deve essere aggiunto alla finestra. Il codice di esempio è:

VBox vbox = new VBox(false, 0);
MenuBar menuBar = new MenuBar();
MenuItem fileItem = new MenuItem("File");
menuBar.append(fileItem);
Menu quitMenu = new Menu();
MenuItem quitItem = new MenuItem("Quit");
quitItem.connect(new MenuItem.Activate() {
 public void onActivate(MenuItem menuItem) {
  Gtk.mainQuit();
 }
});
quitMenu.append(quitItem);
fileItem.setSubmenu(quitMenu);
vbox.packStart(menuBar, false, false, 3);
add(vbox);

e il risultato sarà: QUIT

Le Toolbar, o in Italiano barra delle applicazioni, sono quelle parti di finestra dove poter posizionare dei bottoni che permettono all'utente di accedere a determinate funzionalità in maniera veloce e intuitiva, semplice caso sono i bottoni di "nuovo", "salva", "copia", "incolla", ecc... in qualsiasi editor di testo. Per aggiungere una toolbar ad una finestra bisogna usare le classi Toolbar e ToolButton, con la possibilità di impostare anche una icona di default o di impostare una icona personalizzata, per esempio nel metodo initUI:

Toolbar toolbar = new Toolbar();
ToolButton newtb = new ToolButton(Stock.NEW);
ToolButton opentb = new ToolButton(Stock.OPEN);
ToolButton savetb = new ToolButton(Stock.SAVE);
SeparatorToolItem sep = new SeparatorToolItem();
ToolButton quittb = new ToolButton(Stock.QUIT);
toolbar.insert(newtb, 0);
toolbar.insert(opentb, 1);
toolbar.insert(savetb, 2);
toolbar.insert(sep, 3);
toolbar.insert(quittb, 4);
quittb.connect(new ToolButton.Clicked() {
  public void onClicked(ToolButton toolButton) {
    Gtk.mainQuit();
  }
});
VBox vbox = new VBox(false, 2);
vbox.packStart(toolbar, false, false, 0);
add(vbox);

e il risultato della finestra è:

jgtk_toolbar

La caratteristica di questi bottoni è che non fanno nulla perché non è stato associato nessun evento, per esempio un bottone di uscita associato l'evento onclick:

ToolButton quittb = new ToolButton(Stock.QUIT);
quittb.connect(new ToolButton.Clicked() {
  public void onClicked(ToolButton toolButton) {
    Gtk.mainQuit();
  }
});
toolbar.insert(quittb, 0);

altra proprietà interessante e molto utile dei bottoni nelle Toolbar è la possibilità di disabilitarli run-time, per esempio facendo due bottoni che, quando premuti si auto-disabilitano e abilitano l'altro:

b1.connect(new ToolButton.Clicked() {
  public void onClicked(ToolButton toolButton) {
    b1.setSensitive(false);
    b2.setSensitive(true);
  }
});
b2.connect(new ToolButton.Clicked() {
  public void onClicked(ToolButton toolButton) {
    b2.setSensitive(false);
    b1.setSensitive(true);
  }
});

Un esempio completo di classe che crea una finestra con una toolbar (che non fa nulla) è:

package it.alnao.mygtk.example;

import org.gnome.gtk.Gtk;
import org.gnome.gtk.SeparatorToolItem;
import org.gnome.gtk.Stock;
import org.gnome.gtk.ToolButton;
import org.gnome.gtk.Toolbar;
import org.gnome.gtk.VBox;
import it.alnao.mygtk.AMyGtkWindow;

public class MyToolbar extends AMyGtkWindow {
  public static void main(String[] args) {
    new MyToolbar().run(args, "MyToolbar Example", 250, 100, 10);
  }
  
  @Override
  public void setup() {
    Toolbar toolbar = new Toolbar();
    ToolButton newtb = new ToolButton(Stock.NEW);
    ToolButton opentb = new ToolButton(Stock.OPEN);
    ToolButton savetb = new ToolButton(Stock.SAVE);
    SeparatorToolItem sep = new SeparatorToolItem();
    ToolButton quittb = new ToolButton(Stock.QUIT);
    toolbar.insert(newtb, 0);
    toolbar.insert(opentb, 1);
    toolbar.insert(savetb, 2);
    toolbar.insert(sep, 3);
    toolbar.insert(quittb, 4);
    quittb.connect(new ToolButton.Clicked() {
      public void onClicked(ToolButton toolButton) {
        Gtk.mainQuit();
      }
    });
    savetb.connect(new ToolButton.Clicked() {
      public void onClicked(ToolButton toolButton) {
        toolButton.setSensitive(false);
      }
    });
    VBox vbox = new VBox(false, 2);
    vbox.packStart(toolbar, false, false, 0);
    addWidget(vbox);
  }
}
 

La gestione degli eventi è una delle caratteristiche principali della programmazione Java, si può dire che la più grande differenza tra Java e C++ sia proprio la gestione degli eventi, le applicazioni sviluppate con GTK hanno a disposizione la gestione eventi standard di Java combinata con i componenti grafici, per esempio si può gestire la pressione di un bottone, per associare un evento ad un elemento grafico basta definire una funzione e "collegarla" all'evento, per esempio:

bottoneQuit.connect(new Button.Clicked() {
  public void onClicked(Button button) {
    Gtk.mainQuit();
  }
});

per associare ad un bottone (bottoneQuit) che, alla pressione (clicked) venga lanciato il comando di uscita dall'applicazione. In quasi tutte le applicazioni si trova la gestione dell'uscita dell'applicazione:

connect(new Window.DeleteEvent() {
  public boolean onDeleteEvent(Widget source, Event event) {
    Gtk.mainQuit();
    return false;
  }
});

questo codice gestisce la chiusura dell'applicazione quando viene chiusa la finestra principale, senza questa gestione dell'evento, la finestra verrebbe chiusa ma l'applicazione rimarrebbe avviata in background. Un altro classico esempio di gestione degli eventi è quello di eseguire codice quando un oggetto viene creato, in quando "apertura" è pur sempre un evento, per fare questo si usa il metodo onConfigureEvent:

public class FinestraEsempio extends Window {
  ...
  public boolean onConfigureEvent(Widget widget, EventConfigure eventConfigure) {
    int x = eventConfigure.getX();
    int y = eventConfigure.getY();
    setTitle("X= "+x + " Y= " + y);
    return false;
  }
  ...
}

con questo sempice codice abbiamo catturato l'evento "apertura" e abbiamo modificato il titolo della finestra con larghezza e altezza della finestra stessa, per far funzionare questo codice bisogna però ricordarsi di aggiungere due cose nella classe della finestra bisogna aggiungere una implementazione:

implements Window.ConfigureEvent

e nel metodo di creazione della finestra bisogna aggiungere la connessione agli eventi:

connect(this);

Gli elementi grafici base, chiamati Widgets, sono molti e in questi articoli li vedremo tutti con degli esempi pratici.

Il più semplice oggetto che possiamo mettere in una finestra è la Label, cioè un elemento grafico che visualizza un testo, un esempio:

import org.gnome.gtk.Label;
public class EsempioLabel extends Window {
  public EsempioLabel() {
    setTitle("Esempio Label");
    initUI();
    connect(new Window.DeleteEvent() {
      public boolean onDeleteEvent(Widget source, Event event) {
        Gtk.mainQuit();
        return false;
      }
    });
    setPosition(WindowPosition.CENTER);
    showAll();
  }
  public void initUI() {
    Label lyrics = new Label("prova di testos\n a capo \nEsempio AlNao.it");
    add(lyrics); 
    setBorderWidth(8);
  }
  public static void main(String[] args) {
    Gtk.init(args);
    new EsempioLabel();
    Gtk.main();
  }
}

notare che oltre alla label abbiammo aggiunto anche un bordo per evitare che la label "toccasse" i bordi della finestra.

HSeparator è un classico separatore tra oggetti all'interno di una finestra, che può essere aggiunto in pagina:

public void initUI() { 
  VBox vbox = new VBox(false, VERTICAL_SPACE);
  Label labeluno = new Label("Primo testo" );
  labeluno.setLineWrap(true);
  labeluno.setAlignment(X_ALIGN, Y_ALIGN);
  vbox.packStart(labeluno);
  HSeparator hsep = new HSeparator();
  vbox.packStart(hsep);
  Label labeldue = new Label("Secondo elemento");
  labeldue.setAlignment(X_ALIGN, Y_ALIGN);
  labeldue.setLineWrap(true);
  vbox.packStart(labeldue);
  add(vbox);
  setResizable(false);
  setBorderWidth(BORDER_AROUND);
}

notare che abbiamo dovuto gestire i vari allineamenti dei testi.

CheckButton permette di inserire dei checkbox con un test e di gestire gli eventi dell'elemento, per esempio:

public void initUI() {
  setBorderWidth(10);
  Fixed fixed = new Fixed();
  check = new CheckButton("Mostra il titolo");
  check.setActive(true);
  check.connect(this);
  check.setCanFocus(false);
  fixed.put(check, 50, 50);
  add(fixed);
  setPosition(WindowPosition.CENTER);
}
public void onToggled(ToggleButton toggleButton) { //evento toogled
  if (check.getActive()) {
    setTitle(title);
  } else {
    setTitle("");
  }
}

TextComboBox è la classe da usare per visualizzare le tendine a scelta multipla, un esempio dell'elemento:
TextComboBox cb;
Label label;
public void initUI() {
  connect(new Window.DeleteEvent() {
    public boolean onDeleteEvent(Widget source, Event event) {
      Gtk.mainQuit();
      return false;
    }
  });
  Fixed fixed = new Fixed();
  label = new Label("");
  fixed.put(label, 55, 130);
  cb = new TextComboBox();
  cb.appendText("Ubuntu");
  cb.appendText("Mandriva");
  cb.appendText("Debian");
  cb.appendText("Gentoo");
  cb.connect(this);
  fixed.put(cb, 50, 50);
  add(fixed);
}
public void onChanged(ComboBox comboBox) {
  String text = cb.getActiveText();
  label.setLabel(text);
}
La classe Image permette di inserire in un oggetto box all'interno di una finestra una immagine da un file:
public void initUI() {
  connect(new Window.DeleteEvent() {
    public boolean onDeleteEvent(Widget source, Event event) {
      Gtk.mainQuit();
      return false;
    }
  });
  Image image = new Image("redrock.png");
  int width = image.getRequisition().getWidth();
  int height = image.getRequisition().getHeight();
  setSizeRequest(width, height);
  add(image);
  setBorderWidth(2);
}
 

L'oggetto Entry permette di inserire in una finestra un area di input-text con una singola linea:

public class GEntry extends Window {
 private Label label;
 private Entry entry;
 public GEntry() {
   setTitle("Entry");
   initUI();
   connect(new Window.DeleteEvent() {
     public boolean onDeleteEvent(Widget source, Event event) {
       Gtk.mainQuit();
       return false;
     }
   });
   setDefaultSize(250, 200);
   setPosition(WindowPosition.CENTER);
   showAll();
 }
 public void initUI() {
   label = new Label("...");
   entry = new Entry();
   entry.connect(new Entry.Changed() {
     public void onChanged(Editable editable) {
       label.setLabel(entry.getText());
     }
   });
   Fixed fix = new Fixed();
   fix.put(entry, 60, 100);
   fix.put(label, 60, 40);
   add(fix);
 }
 public static void main(String[] args) {
   Gtk.init(args);
   new GEntry();
   Gtk.main();
  } 
}

L'esempio mostra una finesta con una label ed una input-text, dove la parte più importante è quella dove si definisce l'oggetto di tipo Entry e si definisce l'evento onChange.

La classe Scale permette invece di inserire in una finestra uno slider, un esempio un semplice slider per regolare il volume con una piccola immagine:

public void initUI() {
  loadImages();
  hscale = new HScale(0, 99, 1);
  hscale.setSizeRequest(130, 45);
  hscale.setCanFocus(false);
  image = new Image("mute.png");
  hscale.connect(new HScale.ValueChanged() {
    public void onValueChanged(Range range) {
      int pos = (int) hscale.getValue();
      if (pos == 0) {
        image.setImage(mute);
      } else if (pos > 0 && pos <= 30) {
        image.setImage(min);
      } else if (pos > 30 && pos < 80) {
        image.setImage(med);
      } else {
        image.setImage(max);
      }
    }
  });
  Fixed fixed = new Fixed();
  fixed.put(hscale, 40, 20);
  fixed.put(image, 220, 40);
  add(fixed);
 }
 private void loadImages() {
   try {
     mute = new Pixbuf("mute.png");
     min = new Pixbuf("min.png");
     med = new Pixbuf("med.png");
     max = new Pixbuf("max.png");
   } catch (FileNotFoundException e) {
     e.printStackTrace();
   }
 }

Dove l'istruzione fondamentale è dove è strato definito il HScale

hscale = new HScale(0, 99, 1);

Per recuperare il valore è stata usata l'istruzione:

int pos = (int) hscale.getValue();

La classe ToggleButton definisce il classico bottone normale con due stati: "pressed" e "not pressed", ovviamente la differenze sta nel fatto che un utente abbia cliccato sul bottone stesso, un esempio completo dell'uso di questi bottoni:

import org.gnome.gdk.Color;
import org.gnome.gdk.Event;
import org.gnome.gtk.DrawingArea;
import org.gnome.gtk.Fixed;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.StateType;
import org.gnome.gtk.ToggleButton;
...
public class GToggleButton extends Window implements ToggleButton.Toggled {
 private ToggleButton tb1;
 private ToggleButton tb2;
 private DrawingArea darea;
 private Color color;
...
 public void initUI() {
   color = new Color(0, 0, 0);
   connect(new Window.DeleteEvent() {
     public boolean onDeleteEvent(Widget source, Event event) {
       Gtk.mainQuit();
       return false;
     }
   });
   Fixed fixed = new Fixed();
   tb1 = new ToggleButton("Rosso");
   tb1.setSizeRequest(80, 35);
   tb1.connect(this);
   tb2 = new ToggleButton("Verde");
   tb2.setSizeRequest(80, 35);
   tb2.connect(this);
   darea = new DrawingArea();
   darea.modifyBackground(StateType.NORMAL, Color.BLACK);
   darea.setSizeRequest(150, 150);
   fixed.put(tb1, 20, 20);
   fixed.put(tb2, 20, 65);
   fixed.put(darea, 150, 20);
   add(fixed);
 }
 public void onToggled(ToggleButton toggleButton) {
   int red = color.getRed();
   int green = color.getGreen();
   if ("Rosso".equals(toggleButton.getLabel())) {
     if (toggleButton.getActive()) {
       red = 65535;
     } else {
       red = 0;
     }
   }
   if ("Verde".equals(toggleButton.getLabel())) {
     if (toggleButton.getActive()) {
       green = 65535;
     } else {
       green = 0;
     }
   }
   color = new Color(red, green, 0); //blue=0
   darea.modifyBackground(StateType.NORMAL, color);
 }
 ...
}

In questo esempio abbiamo due bottoni e un area "DrawingArea", il colore di sfondo cambia a seconda del bottone premuto, l'evento onToggled non è associato a nessun bottone perché viene "agganciato" automaticamente grazie all'interfaccia Toggled dichiarata come madre della nostra classe. La definizione del bottone avviene con le semplici istruzioni

tb1 = new ToggleButton("Red");
tb1.setSizeRequest(80, 35);
tb1.connect(this);
Mentre la gestione dell'evento è realizzata con il codice 
if ("Red".equals(toggleButton.getLabel())) {
  if (toggleButton.getActive()) {
    red = 65535;
  } else {
    red = 0;
  }
}

L'oggetto Calendar è semplicamente la gestione dell'oggetto calendario completo, un esempio:

import org.gnome.gtk.Calendar;
..
public class ExampleCalendar extends Window {

 private Calendar calendar;
 private Label label;
...
 private void initUI() {
   VBox vbox = new VBox(false, 1);
   calendar = new Calendar();
   label = new Label(getDate());
   calendar.connect(new Calendar.DaySelected() {
     public void onDaySelected(Calendar calendar) {
       label.setLabel(getDate());
     }
   });
   vbox.add(calendar);
   label.setSizeRequest(-1, 50);
   vbox.add(label);
   add(vbox);
 }
 private String getDate() {
   int year = calendar.getDateYear();
   int month = calendar.getDateMonth();
   int day = calendar.getDateDay();
   String dateLabel = month + "/" + day + "/" + year; 
   return dateLabel;
 }
 ...
}

In questa finestra abbiamo definito il calendario e l'oggetto calendar, la label viene aggiornata ogni volta che viene selezionato un giorno grazie all'evento onDaySelected, il metodo getDate serve solo per formattare la data all'italiana.

Una dei componenti più complessi di quelli a disposizione nell'architettura GTK è la TextView, cioè una sezione dedicata alla visualizzazione e alla modifica di testo formattato, questo componente ha un pattern di tipo Model-View-Controller, dove la classe TextView rappresenta la componente visuale mentre TexBuffer rappresenta la componente model e viene usata per definire lo stile del contenuto della view attraverso l'attributo TextTag, un esempio è:

...
import org.gnome.gtk.TextBuffer;
import org.gnome.gtk.TextIter;
import org.gnome.gtk.TextTag;
import org.gnome.gtk.TextView;
import org.gnome.gtk.Widget;
import org.gnome.gtk.Window;
import org.gnome.gtk.WindowPosition;
import org.gnome.gtk.WrapMode;
import org.gnome.pango.FontDescription;
import org.gnome.pango.Style;
...
public class TextViewExample extends Window {
 public static final string aCapo="\n";
 ...
public void initUI() {
 final TextView view;
 final FontDescription desc;
 final TextBuffer buffer;
 final TextTag colorText, italics, background, mleft;
 TextIter pointer;
 view = new TextView();
 desc = new FontDescription("Sans, 1");
 view.modifyFont(desc);
 buffer = new TextBuffer();
 colorText = new TextTag();
 colorText.setForeground("blue");
 italics = new TextTag();
 italics.setStyle(Style.ITALIC);
 background = new TextTag();
 background.setBackground("lightgray");
 mleft = new TextTag();
 mleft.setLeftMargin(8);
 pointer = buffer.getIterStart();
 buffer.insert(pointer, "Testo normale" + aCapo);
 buffer.insert(pointer, "Colore rosso" + aCapo, new TextTag[] {colorText, mleft});
 buffer.insert(pointer, "Colore con uno sfondo" + aCapo, background);
 buffer.insert(pointer, "Testo con stile italic", italics);
 view.setBuffer(buffer);
 view.setPaddingAboveParagraph(5);
 add(view);
 }
 ...
}

In questo esempio vediamo il testo con diversi stili, nel dettaglio per creare l'oggeto basta chiamare il costruttore della TextView:

view = new TextView();

per poi aggiungere alla TextView la caratteristica per formattare il testo con carattere Sans a dimensione 11

desc = new FontDescription("Sans, 11");
view.modifyFont(desc);

Poi nell'esempio sono stati definiti vari porzioni di testo con caratteristiche diverso con la classe TextTag e i vari metodi a disposizione come setForeground, setStyle e setBackground.

L'oggetto ListView invece permette di disegnare in una finestra un classico elenco e si usa l'oggetto ListStore per definire i dati, per esempio:

...
import org.gnome.gtk.CellRendererText;
import org.gnome.gtk.DataColumn;
import org.gnome.gtk.DataColumnString;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.ListStore;
import org.gnome.gtk.Statusbar;
import org.gnome.gtk.TreeIter;
import org.gnome.gtk.TreePath;
import org.gnome.gtk.TreeView;
import org.gnome.gtk.TreeViewColumn;
import org.gnome.gtk.VBox;
import org.gnome.gtk.Widget;
import org.gnome.gtk.Window;
import org.gnome.gtk.WindowPosition;
public class ListCapView extends Window {
  private Citta[] capItaliani = {
    new Citta("Roma", "Lazio", "00118"),
    new Citta("Milano", "Lombardia", "20121"),
    new Citta("Torino", "Piemonte", "10121"),
    new Citta("Napoli", "Campania", "80121"),
    new Citta("Venezia", "Veneto", "30121"),
    new Citta("Palermo", "Sicilia", "90121") 
  };
  private Statusbar statusbar;
  ...
  public void initUI() {
    final TreeView view;
    final ListStore model;
    TreeIter row;
    CellRendererText renderer;
    TreeViewColumn column;
    final DataColumnString nomeCitta;
    final DataColumnString nomeRegione;
    final DataColumnString cap;
    statusbar = new Statusbar();
    model = new ListStore(new DataColumn[] {
      nomeCitta = new DataColumnString(),
      nomeRegione = new DataColumnString(),
      cap = new DataColumnString(),
    });
    for (Citta act : capItaliani) {
      row = model.appendRow();
      model.setValue(row, nomeCitta, act.name);
      model.setValue(row, nomeRegione, act.place);
      model.setValue(row, cap, String.valueOf(act.year));
    }
    view = new TreeView(model);
    column = view.appendColumn();
    column.setTitle("Citta");
    renderer = new CellRendererText(column);
    renderer.setText(nomeCitta);
    column = view.appendColumn();
    column.setTitle("Regione");
    renderer = new CellRendererText(column);
    renderer.setText(nomeRegione);
    column = view.appendColumn();
    column.setTitle("CAP");
    renderer = new CellRendererText(column);
    renderer.setText(cap);
    view.connect(new TreeView.RowActivated() {
      public void onRowActivated(TreeView treeView,TreePath treePath,
      TreeViewColumn treeViewColumn) {
        final TreeIter row;
        final String place;
        final String name;
        final String year;
        final String text;
        row = model.getIter(treePath);
        place = model.getValue(row, nomeRegione);
        name = model.getValue(row, nomeCitta);
        year = model.getValue(row, cap);
        text = name + ", " + place + ", " + year;
        statusbar.setMessage(text);
      }
    });
    VBox vbox = new VBox(false, 0);
    vbox.packStart(view);
    vbox.packStart(statusbar, false, false, 0);
    add(vbox);
  }
  class Citta {
    public String nomeRegione;
    public String nomeCitta;
    public int cap;
    Citta(String nomeRegione, String nomeCitta, int cap) {
      this.nomeRegione = nomeRegione;
      this.nomeCitta = nomeCitta;
      this.cap = cap;
    }
    ...
  }
}

Nell'esempio abbiamo creato un bean Citta e abbiamo inserito nell'oggetto capItaliani l'elenco di sei città italiane (a caso), poi abbiamo dovuto definire la struttura del ListStore e eseguire un ciclo sugli elementi della lista per popolare l'oggetto model, poi basta definire i dettagli dell'elenco come le colonne e il render usando l'oggetto di tipo DataColumnString. Nell'esempio abbiamo anche collegato l'evento onRowActivated che permette di gestire il focus di una singola riga con il recupero dei valori di quella riga per visualizzare un messaggio o valorizzare un testo nella barra di stato.

La classe Tree permette di creare un oggetto grafico gerarchico ad albero, un esempio pratico:

...
import org.gnome.gdk.Event;
import org.gnome.gtk.CellRendererText;
import org.gnome.gtk.DataColumn;
import org.gnome.gtk.DataColumnString;
import org.gnome.gtk.Gtk;
import org.gnome.gtk.Statusbar;
import org.gnome.gtk.TreeIter;
import org.gnome.gtk.TreePath;
import org.gnome.gtk.TreeStore;
import org.gnome.gtk.TreeView;
import org.gnome.gtk.TreeViewColumn;
public class TreeExample extends Window {
  private DataColumnString dataCol;
  private TreeStore model;
  private TreeIter row;
  private TreeView view;
  private TreeIter rowChild;
  private TreeViewColumn column;
  private CellRendererText renderer;
  private Statusbar statusbar;
  ...
  public void initUI() {
    statusbar = new Statusbar();
    model = new TreeStore(new DataColumn[] {
      dataCol = new DataColumnString(),
    });
    row = model.appendRow();
    model.setValue(row, dataCol, "Web languages");
    rowChild = model.appendChild( row );
    model.setValue(rowChild, dataCol, "HTML"); 
    rowChild = model.appendChild( row );
    model.setValue(rowChild, dataCol, "CSS");
    rowChild = model.appendChild( row );
    model.setValue(rowChild, dataCol, "Javascript");
    row = model.appendRow();
    model.setValue(row, dataCol, "Compiling languages");
    rowChild = model.appendChild( row );
    model.setValue( rowChild, dataCol, "Java"); 
    rowChild = model.appendChild( row );
    model.setValue( rowChild, dataCol, "C++");
    rowChild = model.appendChild( row );
    model.setValue( rowChild, dataCol, "C");
    rowChild = model.appendChild( row );
    model.setValue( rowChild, dataCol, "Pascal"); 
    view = new TreeView(model);
    column = view.appendColumn();
    column.setTitle("Programming Languages");
    renderer = new CellRendererText(column);
    renderer.setText(dataCol); 
    view.connect(new TreeView.RowActivated() {
      public void onRowActivated(TreeView treeView,TreePath treePath,
      TreeViewColumn treeViewColumn) {
        final TreeIter row;
        final String text;
        row = model.getIter(treePath);
        text = model.getValue(row, dataCol);
        statusbar.setMessage(text);
      }
    });
    VBox vbox = new VBox(false, 0);
    vbox.packStart(view);
    vbox.packStart(statusbar, false, false, 0);
    add(vbox);
  } 
  ...
}

Anche in questo caso abbiamo gestito i dati con i vari oggetti dell'architettura, come TreeStore e DataColumn e poi abbiamo gestito l'evento onRowActivated per recuperare il valore selezionato e visualizzarlo nella barra di stato.

Per Dialogs si intendono tutte le finestre di dialogo che permettono di visualizzare vari messaggi all'utente, lo standard prevede un testo e una immagine. Un esempio:
...
import org.gnome.gtk.MessageDialog;
import org.gnome.gtk.MessageType;
...
public class Myessages extends Window {
...
 public void initUI() {
   ...
   Button info = new Button("Information");
   info.connect(new Button.Clicked() {
     public void onClicked(Button button) {
       MessageDialog md = new MessageDialog(null, true,
       MessageType.INFO, 
       ButtonsType.CLOSE, "Download completed");
       md.setPosition(WindowPosition.CENTER);
       md.run();
       md.hide();
     }
   });
   ...
   Button warn = new Button("Warning");
   warn.connect(new Button.Clicked() {
     public void onClicked(Button button) {
       MessageDialog md = new MessageDialog(parent, true,
       MessageType.WARNING, 
       ButtonsType.CLOSE, "Unallowed operation");
       md.setPosition(WindowPosition.CENTER);
       md.run();
       md.hide();
     }
   });
   Button ques = new Button("Question");
   ques.connect(new Button.Clicked() {
     public void onClicked(Button button) {
       MessageDialog md = new MessageDialog(null, true,
       MessageType.QUESTION, 
       ButtonsType.CLOSE, "Are you sure to quit?");
       md.setPosition(WindowPosition.CENTER);
       md.run();
       md.hide();
     }
   });
   Button erro = new Button("Error");
   erro.connect(new Button.Clicked() {
     public void onClicked(Button button) {
       MessageDialog md = new MessageDialog (null, true, 
       MessageType.ERROR, 
       ButtonsType.CLOSE, "Error loading file");
       md.setPosition(WindowPosition.CENTER);
       md.run();
       md.hide();
     } 
   });
   ...
   table.attach(info, 0, 1, 0, 1);
   table.attach(warn, 1, 2, 0, 1);
   table.attach(ques, 0, 1, 1, 2);
   table.attach(erro, 1, 2, 1, 2);
   add(table);
   ...
 }
...
}
dove in questo esempio creiamo quattro diversi dialog, uno per tipo: informativo, warning, interrogativa e messaggio di errore. Da notare che, in queste finestre di dialogo, è presente solo il bottone chiudi ma non altri bottoni, come un "SI/NO". All'interno della libreria Dialog è presente anche la AboutDialog per creare i popup dedicati, un esempio:
...
import org.gnome.gtk.AboutDialog;
...
public class MyExampleAboutDialog extends Window implements Button.Clicked {
  ...
  public void initUI() {
    try {
      logo = new Pixbuf("logo.png");
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
    Button button = new Button("About");
    button.connect(this);
    Fixed fix = new Fixed();
    fix.put(button, 20, 20);
    add(fix);
  }
 
  public void onClicked(Button button) {
    AboutDialog about = new AboutDialog();
    about.setProgramName("MyExampleAboutDialog");
    about.setVersion("0.1");
    about.setCopyright("(c) Alberto Nao");
    about.setComments("Questo è un semplice esempio di AboutDialog");
    about.setLogo(logo);
    about.setPosition(WindowPosition.CENTER);
    about.run();
    about.hide();
  }
  ...
}

Pango è una libreria open source che può essere usata nelle finestre gtk per il rendering grafico di test in alta qualità in qualsiasi formato, e in particolare per la visualizzazione di test in font non standard, un esempio pratico:

...
import org.gnome.pango.FontDescription;
public class PangoWin extends Window {
 ...
 public void initUI() {
   String text = "Questo è un testo scritto\nanche in pià righe";
   Label label = new Label(text);
   FontDescription fontdesc = new FontDescription("Purisa 9");
   label.modifyFont(fontdesc);
   Fixed fix = new Fixed();
   fix.put(label, 5, 5);
   add(fix);
 }
 ...
}

la cosa importante è l'importazione della libreria FontDescription e nell'invocazione del metodo modifyFont che indica alla finestra di visualizzare la label in maniera "diversa" dalla default.

Altro esempio con l'uso dei markup:

...
import org.gnome.pango.FontDescription;
import org.gnome.pango.Layout;
public class PangoWin extends Window {
  ...
  String quote = "<span foreground='blue'>Testo con uno stile</span>";
  ...
  public void initUI() {
    DrawingArea darea = new DrawingArea();
    darea.connect(this);
    darea.queueDraw();
    add(darea);
    setBorderWidth(5);
  }
  public boolean onExposeEvent(Widget widget, EventExpose eventExpose) {
    final Context cr;
    final Layout layout;
    final FontDescription desc;
    cr = new Context(widget.getWindow());
    layout = new Layout(cr);
    desc = new FontDescription("Sans 12");
    layout.setFontDescription(desc);
    layout.setMarkup(quote);
    widget.setSizeRequest(layout.getPixelWidth(), layout.getPixelHeight());
    cr.showLayout(layout);
    return false;
  }
  ...
}
Cairo è una sotto-libreria per il disegno 2D vettoriale e può essere usato per disegnare widget, grafici o effetti di animazioni. Un semplice esempio:
import org.freedesktop.cairo.Context;
import org.gnome.gtk.DrawingArea;
...
public void initUI() {
  DrawingArea darea = new DrawingArea();
  darea.connect(this);
  add(darea);
}
public boolean onExposeEvent(Widget widget, EventExpose eventExpose) {
  final Context cr;
  cr = new Context(widget.getWindow()); 
  drawShape(cr);
  return false;
}
public void drawShape(Context cr) {
  cr.setLineWidth(9);
  cr.setSource(0.7, 0.2, 0.0);
  int width, height;
  width = getAllocation().getWidth();
  height = getAllocation().getHeight();
  cr.translate(width/2, height/2); //questo per centrare il cerchio
  cr.arc(0, 0, (width < height ? width : height) / 2 - 10, 0, 2*Math.PI); //cerchio
  cr.strokePreserve();
  cr.setSource(0.3, 0.4, 0.6);
  cr.fill();
} 
...
il risultato di questo esempio è la creazione di una porzione di finestra dove è disegnato un cerchio blu con un bordo rosso spesso, l'utilizzo del metodo strokePreserve serve per preservare le misure e passare al secondo elemento (prima disegna l'arco rosso e poi riempie con il blu). Le forme utilizzabili sono:
 cr.rectangle(20, 20, 120, 80);
 cr.rectangle(180, 20, 80, 80);
 cr.arc(330, 60, 40, 0, 2*Math.PI);
Ma è possibile creare forme matriciali, per esempio:
 Matrix mat = new Matrix();
 mat.translate(220, 180); 
 mat.scale(1, 0.7);
 cr.transform(mat);
 cr.arc(0, 0, 50, 0, 2*Math.PI);
 cr.fill();
Per quanto riguarda i colori è utilizzata lo standard RGB ma con un range da 0 a 1:
 cr.setSource(0.5, 0.65, 0.45); //verde oliva
 cr.setSource(0.16, 0.7, 0.9); //azzurro
 cr.setSource(0.274, 0.262, 0.48); //blu scuro
 cr.setSource(0.5, 0.39, 0.33); //marrone scuro
 cr.setSource(0.99, 0.83, 0.24); //giallo spento
 cr.setSource(0.95, 0.38, 0.27); //rosso 
 cr.setSource(0.85, 0.57, 0.21); //marrone chiaro
 cr.setSource(0.25, 0.04, 0.73); //blu
 cr.setSource(0.12, 0.08, 0.03); //quasi nero
Essendo dei valori numeri, è possibile creare algoritmi per la gestione delle trasparenze: aggiungendo un parametro alla setSource, per esempio
for (int i = 1; i<=10; i++) {
  cr.setSource(0, 0, 1, 0.1*i);
  cr.rectangle(50*i, 20, 40, 40);
  cr.fill();
}
disegna nella finestra dei rettangoli con diverse trasparenze.

Nel seguente esempio viene disegnata una "ciambella" usando l'istruzione arc all'interno di un ciclo for, infatti è possibile utilizzare le iterazioni per il disegno "vettoriale":

...
public void initUI() {
 DrawingArea darea = new DrawingArea();
 darea.connect(this);
 add(darea);
}
public boolean onExposeEvent(Widget widget, EventExpose eventExpose) {
 final Context cr; 
 cr = new Context(widget.getWindow()); 
 drawDonut(cr);
 return false;
}
public void drawDonut(Context cr) {
 int width = this.getWindow().getWidth();
 int height = this.getWindow().getHeight();
 cr.setLineWidth(0.5);
 cr.translate(width/2, height/2);
 cr.arc( 0, 0, 120, 0, 2 * Math.PI);
 cr.stroke();
 cr.save();
 for ( int i = 0; i < 36; i++) {
  Matrix mat = new Matrix();
  mat.rotate(i*Math.PI/36);
  mat.scale(0.3, 1);
  cr.transform(mat);
  cr.arc(0, 0, 120, 0, 2 * Math.PI);
  cr.restore();
  cr.stroke();
  cr.save();
 }
}
...

e il risultato è

Per i gradienti invece è possibile utilizzare le librerie evolute di Cairo per il disegno 2D, per esempio:

... 
public void drawGradients(Context cr) {
 LinearPattern lg1 = new LinearPattern(0.0, 0.0, 350.0, 350.0);
 int count = 1;
 for (double j=0.1; j<1.0; j+= 0.1) {
  if (count % 2 != 0) {
   lg1.addColorStopRGB(j, 0, 0, 0);
  } else {
   lg1.addColorStopRGB(j, 1, 0, 0);
  }
  count++;
 }
 cr.rectangle(20, 20, 300, 100);
 cr.setSource(lg1);
 cr.fill();
 LinearPattern lg2 = new LinearPattern(0.0, 0.0, 350.0, 0);
 count = 1;
 for (double i=0.05; i<0.95; i+= 0.025) {
  if (count % 2 != 0) {
   lg2.addColorStopRGB(i, 0, 0, 0);
  } else {
   lg2.addColorStopRGB(i, 0, 0, 1);
  }
  count++;
 }
 cr.rectangle(20, 140, 300, 100);
 cr.setSource(lg2);
 cr.fill();
 LinearPattern lg3 = new LinearPattern(20.0, 260.0, 20.0, 360.0);
 lg3.addColorStopRGB(0.1, 0, 0, 0 );
 lg3.addColorStopRGB(0.5, 1, 1, 0);
 lg3.addColorStopRGB(0.9, 0, 0, 0 );
 cr.rectangle(20, 260, 300, 100);
 cr.setSource(lg3);
 cr.fill();
}
...

Nell'esempio sono disegnati tre diversi rettangoli con tre gradienti diversi, il risultato finale è:

Con le librerie Cairo è possibile anche creare delle animazioni anche se queste risultano sempre in 2D, in un semplice esempio di una stella che si muove e ruota, nell'esempio è utilizzata anche la libreria standard java.util.Timer:

...
import java.util.Timer;
import java.util.TimerTask;
import org.freedesktop.cairo.Context;
import org.freedesktop.cairo.Matrix;
...
public class GStar extends Window implements Widget.ExposeEvent {
  private static Timer timer;
  private int count;
  private double angle = 0;
  private double scale = 1;
  private double delta = 0.01;
  double points[][] = { 
    { 0, 85 }, { 75, 75 }, { 100, 10 }, { 125, 75 }, 
    { 200, 85 }, { 150, 125 }, { 160, 190 }, { 100, 150 }, 
    { 40, 190 }, { 50, 125 }, { 0, 85 } 
 };
 public GStar() {
   ...
   timer = new Timer();
   timer.scheduleAtFixedRate(new ScheduleTask(), 100, 20);
   count = 0;
   ...
 }
 public void drawStar(Context cr) {
   int width = this.getWindow().getWidth();
   int height = this.getWindow().getHeight();
   cr.setSource(0, 0.44, 0.7);
   cr.setLineWidth(1);
   Matrix mat = new Matrix();
   mat.translate(width/2, height/2);
   mat.rotate(angle);
   mat.scale(scale, scale);
   cr.transform(mat);
   for ( int i = 0; i < 10; i++ ) {
     cr.lineTo(points[i][0], points[i][1]);
   }
   cr.fill();
   cr.stroke();
   if ( scale < 0.01 ) {
     delta = -delta;
   } else if (scale > 0.99) {
     delta = -delta;
   }
   scale += delta;
   angle += 0.01;
 }
 public boolean onExposeEvent(Widget widget, EventExpose eventExpose) {
   Context cr = new Context(widget.getWindow());
   drawStar(cr);
   return false;
 }

 class ScheduleTask extends TimerTask { //inner class
   public void run() {
     count++;
     queueDraw();
   }
 } 
}

Un altro semplice esempio di una rotella wainting disegnata utilizzando delle semplici linee e l'effetto gradiente e trasparenza creando l'effetto del movimento.

...
private final double[][] trs = { //posizioni delle linee
 { 0.0, 0.15, 0.30, 0.5, 0.65, 0.80, 0.9, 1.0 },
 { 1.0, 0.0, 0.15, 0.30, 0.5, 0.65, 0.8, 0.9 },
 { 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65, 0.8 },
 { 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5, 0.65 },
 { 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3, 0.5 },
 { 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15, 0.3 },
 { 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, 0.15 },
 { 0.15, 0.3, 0.5, 0.65, 0.8, 0.9, 1.0, 0.0, }
 };
...
public GWaiting() {
  setPosition(WindowPosition.CENTER);
  timer = new Timer();
  timer.scheduleAtFixedRate(new ScheduleTask(), 100, 80);
  count = 0;
  ...
}
private void drawWaiting(Context cr) {
  int w = this.getWidth();
  int h = this.getHeight();
  cr.translate(w/2, h/2);
  for (int i = 0; i < 8; i++) {
  cr.setLineWidth(3);
  cr.setSource(0, 0, 0, trs[count%8][i]);
  cr.moveTo(0, -10);
  cr.lineTo(0, -40);
  cr.rotate(Math.PI/4f);
  cr.stroke();
}
class ScheduleTask extends TimerTask {
  public void run() {
    count++;
    queueDraw();
  }
} 
...

e il risultato di questo esempio è:

Waiting

I contenuti di AlNao.it potrebbero avere inesattezze o refusi. Non potrà in alcun caso e per qualsiasi motivo essere ritenuta responsabile di eventuali imprecisioni ed errori né di danni causati. Il sito web e tutte le informazioni ed i contenuti in esso pubblicati potranno essere modificati in qualsiasi momento e di volta in volta e senza preavviso. Poiché ogni materiale sarà scaricato o altrimenti ottenuto attraverso l’uso del servizio a scelta e a rischio dell’utente, ogni responsabilità per eventuali danni a sistemi di computer o perdite di dati risultanti dalle operazioni di scarico effettuato dall'utente, ricade sull'utente stesso e non potrà essere imputata ad AlNao.it che declina ogni responsabilità per eventuali danni derivanti dall'inaccessibilità ai servizi presenti sul sito o da eventuali danni causati da virus, file danneggiati, errori, omissioni, interruzioni del servizio, cancellazioni dei contenuti, problemi connessi alla rete, ai provider o a collegamenti telefonici e/o telematici, ad accessi non autorizzati, ad alterazioni di dati, al mancato e/o difettoso funzionamento delle apparecchiature elettroniche dell’utente stesso.

AlNao.it ha adottato ogni possibile accorgimento al fine di evitare che siano pubblicati, nel sito web, contenuti che descrivano o rappresentino scene o situazioni inappropriate o tali che, secondo la sensibilità degli utenti, possano essere ritenuti lesivi delle convinzioni civili, dei diritti umani e della dignità delle persone, in tutte le sue forme ed espressioni. In ogni caso non garantisce che i contenuti del sito web siano appropriati o leciti in altri Paesi, al di fuori dell’Italia. Tuttavia, qualora tali contenuti siano ritenuti non leciti o illegali in alcuni di questi Paesi, ti preghiamo di evitare di accedere al nostro sito e ove scegliessi, in ogni caso, di accedervi, ti informiamo che l’uso che deciderai di fare dei servizi forniti dal sito sarà di tua esclusiva e personale responsabilità. L’utente sarà esclusivo responsabile della valutazione delle informazioni e del contenuto ottenibile mediante il sito web. Il sito web e tutte le informazioni ed i contenuti in esso pubblicati potranno essere modificati in qualsiasi momento e di volta in volta e senza preavviso.

Le pagine di questo sito sono protette dal diritto d’autore (copyright). In particolare a norma della legge sul diritto d’autore e il contenuto del sito è protetto contro duplicazioni, traduzioni, inserimento o trasformazione dello stesso in altri media, incluso l’inserimento o la trasformazione con mezzi elettronici. La riproduzione e lo sfruttamento economico di tutto o di parte del contenuto di questo sito sono consentite solo a seguito del consenso scritto dell’avente diritto. Sia il contenuto che la struttura del sito sono protetti dal diritto d’autore. In particolare, la duplicazione di informazioni o dati, l’uso dei testi o di parte di essi o delle immagini contenute nel sito (eccetto per le foto ad uso stampa) è consentita solo previo consenso scritto dell’avente diritto. Anche le illustrazioni, a norma dell’art. 1 della legge 633/1941 – e successive modifiche e integrazioni – sono protette dal diritto d’autore. Il diritto di pubblicazione e riproduzione di questi disegni è di titolarità dell’avente diritto. Il diritto d’autore sui disegni rimane in vigore anche per i disegni automaticamente o manualmente aggiunti a un archivio. Nulla di quanto contenuto in questo sito vale come concessione a terzi dei diritti di proprietà industriale ed intellettuale indicati in questa sezione. Ci riserviamo tutti i diritti di proprietà intellettuale del sito web.

Marchi ed immagini riferibili a soggetti terzi utilizzati in questo sito non appartengono a AlNao.it e sono da ritenersi di proprietà esclusiva dei rispettivi titolari. Le denominazioni dei prodotti pubblicati su questo sito web o delle società menzionate, anche qualora non siano indicate con il simbolo identificativo della registrazione del marchio sono marchi di titolarità di terzi e sono protetti dalla legge sui marchi (D.lgs. 30/2005 e successive modifiche e integrazioni) e dalle norme in tema di concorrenza sleale. Qualsiasi riproduzione degli stessi è da ritenersi vietata ai sensi di legge. In particolare è espressamente vietato qualsiasi uso di questi marchi senza il preventivo consenso scritto del relativo titolare ed, in particolare, è vietato utilizzarli in modo da creare confusione tra i consumatori in merito all'origine dei prodotti o per finalità di sponsorizzazione, nonché in qualsiasi modo tale da svilire e discreditare il titolare del marchio. Tutti i diritti che non sono espressamente concessi sono riservati al titolare del marchio.

In aggiunta a quanto indicato in altre previsioni delle Condizioni Generali d’Uso, AlNao.it non potrà essere ritenuta in alcun caso responsabile di alcun danno derivante dall'utilizzo o dall'impossibilità di utilizzare il sito web, i contenuti, le informazioni o connessi alla qualità degli stessi.