Creare un back-end REST con Spark

Lo scopo di questo post è di illustrare il funzionamento di Spark: un micro-framework che ci permette di creare velocemente uno strato di servizi REST. Spark è il porting Java di Sinatra, famoso micro-framework del mondo Ruby. Grazie a Spark è possibile creare un web server REST in poche righe, come possiamo vedere in questo semplice esempio:

public class App
{
   public static void main(String[] args)
   {
       Spark.get("/hello", (request, response) -> "Hello World");
   }
}

Lanciando questo main, verrà attivato un web server alla porta 4567. Invocando la route “hello” otterremo, per l’appunto, il canonico saluto per sviluppatori.

Hello World con Spark!

Come si può facilmente notare, per creare un route REST basta il metodo get. I due parametri richiesti sono l’URL di risposta e una lambda che rappresenta la business logic legata all’URL dichiarato. Ovviamente questa route risponde al verbo HTTP GET ed esistono tanti metodi della classe Spark quanti sono i verbi HTTP.

Setup

Prima di addentrarci nei successivi esempi, occupiamoci del setup di questa libreria. Analizziamo insieme il pom.xml di un progetto Spark.


    
        com.sparkjava
        spark-core
        2.0.0
    
    
        com.google.code.gson
        gson
        2.2.4
    

Notiamo che l’unica dipendenza è Spark stessa, la libreria si porta dietro già tutto quello che serve. In particolare il web server utilizzato è un Jetty embedded, esattamente la versione 9.0.2. L’altra dipendenza che utilizzeremo è : API di Google per la creazione/parsing di JSON che sfrutteremo nel prossimo esempio. Andremo a gestire una risorsa REST, un piccolo gruppo di utenti definito da questo Active Record:

public class User implements Serializable{

        private static final long serialVersionUID = 1L;

        private static List users = new ArrayList();

        static{
                users.add(new User(0, "Solid Snake"));
                users.add(new User(1, "Vulcan Raven"));
                users.add(new User(2, "Meryl Silverburgh"));
                users.add(new User(3, "Hal Emmerich"));
                users.add(new User(4, "Frank Jaeger"));
        }

        private Integer id;
        private String name;

        public User(Integer id, String name) {
                super();
                this.id = id;
                this.name = name;
        }

        public User() {}

        public Integer getId() {
                return id;
        }

        public void setId(Integer id) {
                this.id = id;
        }

        public String getName() {
                return name;
        }

        public void setName(String name) {
                this.name = name;
        }

        public static List getAll(){
                return users;

        }

        public static User get(final Integer id){
                return users.stream().filter((p)->p.getId().equals(id)).findFirst().get();
        }

        public static User store(User p){
                if(p.getId() == null){
                        User maxIdPerson = users.stream().max((p1,p2)->Integer.compare(p1.getId(), p2.getId())).get();
                        p.setId(maxIdPerson.getId()+1);
                        users.add(p);
                }else{
                        users.set(p.getId(), p);
                }

                return p;
        }

        public static void delete(User p){
                users.remove(p);
        }
}

Questo invece è il codice delle route che andiamo a definire grazie agli altri metodi di Spark:

public class App
{

    private static Gson GSON = new GsonBuilder().create();

    public static void main( String[] args )
    {
        Spark.get("/hello", (request, response) -> "Hello World");

        Spark.get("/user/:id",  (request, response) -> {
                Integer id = Integer.parseInt(request.params("id"));
                return GSON.toJson(User.get(id));
        });

        Spark.post("/user",  (request, response) -> {
                User toStore = null;
                        try {
                                toStore = GSON.fromJson(request.body(), User.class);
                        } catch (JsonSyntaxException e) {
                                response.status(400);
                                return "INVALID JSON";
                        }

                        if(toStore.getId() != null){
                                response.status(400);
                                return "ID PROVIDED DURING CREATE";
                        }else{
                                User.store(toStore);
                        return GSON.toJson(toStore);
                        }
        });

        Spark.put("/user/:id",  (request, response) -> {
                if(User.get(Integer.parseInt(request.params("id"))) == null){
                        response.status(404);
                        return "NOT_FOUND";
                }else{
                        User toStore = null;
                        try {
                                toStore = GSON.fromJson(request.body(), User.class);
                        } catch (JsonSyntaxException e) {
                                response.status(400);
                                return "INVALID JSON";
                        }
                        User.store(toStore);
                        return GSON.toJson(toStore);
                }
        });

        Spark.delete("/user/:id", (request, response) -> {
                User user = User.get(Integer.parseInt(request.params("id")));
                if(user == null){
                        response.status(404);
                        return "NOT_FOUND";
                }else{
                        User.delete(user);
                        return "USER DELETED";
                }
        });
    }
}

Come abbiamo accennato in precedenza abbiamo utilizzato i metodi post,put,delete per gestire gli altri verbi HTTP.

Autenticazione

Spark ci permette di inserire all’interno della catena HTTP dei Filter, i quali ci consentono di modificare il comportamento di tutte o di alcune delle route. Questi filtri possono essere registrati in modo che siano invocati prima o dopo le route stesse. La registrazione avviene tramite i metodi before e after. In questo prossimo esempio vediamo come agganciare una logica autorizzativa a tutte le route che rispondono ai verbi POST, PUT e DELETE.

Spark.before((request,response)->{
        String method = request.requestMethod();
        if(method.equals("POST") || method.equals("PUT") || method.equals("DELETE")){
                String authentication = request.headers("Authentication");
                if(!"PASSWORD".equals(authentication)){
                    Spark.halt(401, "User Unauthorized");
                }
        }
});

CORS

Come ultimo esempio abiliteremo il CORS in un server Spark. CORS sta per “Cross-origin resource sharing” e indica la possibilità di accedere a risorse REST da domini diversi da quelle di origine. Vi può capitare con delle app che richiedono dati ad un server, oppure con una web app AngularJS che non è hostata sul server che espone i servizi REST. Per attivare questa funzionalità dobbiamo creare una route OPTIONS (che sia in ascolto su qualsiasi URL) che specifichi quali headers e verbi sono autorizzati dal nostro server: nell’esempio seguente verranno semplicemente ritornati tutti quelli richiesti dal client. Aggiungeremo poi un filtro per aggiungere l’header “Access-Control-Allow-Origin” per indicare che l’accesso è autorizzato a tutti i client.

Spark.options("/*", (request,response)->{

    String accessControlRequestHeaders = request.headers("Access-Control-Request-Headers");
    if (accessControlRequestHeaders != null) {
        response.header("Access-Control-Allow-Headers", accessControlRequestHeaders);
    }

    String accessControlRequestMethod = request.headers("Access-Control-Request-Method");
    if(accessControlRequestMethod != null){
        response.header("Access-Control-Allow-Methods", accessControlRequestMethod);
    }

    return "OK";
});

Spark.before((request,response)->{
    response.header("Access-Control-Allow-Origin", "*");
});

Conclusioni

Spark è una libreria veramente utile quando abbiamo bisogno di creare in maniera molto lean un server web REST. Ovviamente il suo complementare è un software HTML5 + Javascript che faccia da front-end. A questo indirizzo è presente un interessante tutorial su come creare un’applicazione Spark+AngularJS da deployare su una macchina OpenShift. Ovviamente Spark non ha tutte le potenzialità di una libreria JAX-RS come , ma non lo vedo come un difetto: semplicemente ha un altro target di progetto. Unica nota che qualcuno potrebbe trovare dolente è la dipendenza da Java 8, senza l’ultima JVM infatti non è possibile utilizzare questo micro-framework. Se quindi vi trovate in un ambiente legacy magari incollati a Java 6 dovete cercare altrove. Per chi fosse interessato qui, sul mio blog personale, c’è un piccolo tutorial su RESTEasy. L’esempio implementato è esattamente lo stesso, in questo modo è possibile capire pregi/difetti di entrambi le API.

Potete consultare il codice di questo post al mio account GitHub. Alla Prossima.

Francesco Strazzullo

Faccio il Front-end engineer per extrategy dove mi occupo di applicazioni Angular, React e applicazioni mobile. Da sempre sono appassionato di Architetture software e cerco di applicarle in maniera innovativa allo sviluppo frontend in generale.