Getting

Groovy

with

Spark Java

Todd Sharp

@recursivecodes

Who Is This Guy?

@recursivecodes

Why Am I Here?

To Learn About The Spark Java Framework

  • What It Is
  • What It Isn't
  • What It Is Good For
  • What It Isn't Good For
  • How To Use Groovy With It

@recursivecodes

Disclaimer:

If you haven't figured it out yet, this presentation is NOT about...

@recursivecodes

What Is Spark Java?

   Spark Framework is a simple and expressive Java/Kotlin web framework DSL built for rapid development. Sparks intention is to provide an alternative for Kotlin/Java developers that want to develop their web applications as expressive as possible and with minimal boilerplate. With a clear philosophy Spark is designed not only to make you more productive, but also to make your code better under the influence of Spark’s sleek, declarative and expressive syntax.

@recursivecodes

Without The Marketing Speak...

  • Lightweight Framework
  • Microservice Friendly
  • Easy To Use
  • Embedded Jetty
  • Many Templating Options

@recursivecodes

Before We Begin...

@recursivecodes

Why Groovy?

It's Less Verbose Than Java

  • public modifier implied
  • Semi-colons optional
  • Parenthesis optional
  • Brackets optional
  • return keyword optional
  • etc...

The GDK

A layer on top of the JDK to enhance the language and make certain things easier to do.
 

  • Files/IO
  • Lists
  • Maps
  • Etc..

The GDK

// reading a file
new File(baseDir, "eps3.3_metadata.par2").eachLine { line ->
    println line
}
// writing a file
new File(baseDir, "eps3.8_stage3.torrent").withWriter('utf-8') { writer ->
    writer.writeLine "Are you seeing this?"
}
// executing external processes
def fiveNine = "ecorp".execute()             
// make an http request
"http://whoismrrobot.com".toURL().text

Dynamic Typing Is Concise And Powerful

def groovy = 'language'

// vs

String groovy = 'language'

def sayHello(name) {
    name ? "Hello, ${name}" : 'Hello, Friend'
}

// vs

public String sayHello() {
    return "Hello, Friend";
}

public String sayHello(String name) {
    return "Hello, " + name;
}

Implicit Getters/Setters

class User {
    def firstName
    def lastName
}

def user = new User()
user.firstName = 'Todd'
user.lastName = 'Sharp'

assert user.firstName + ' ' + user.lastName == 'Todd Sharp'
 
// true

Null Safe Operator

user?.homeAddress?.state?.abbreviation

Spread Operator

class Car {
    String make
    String model
}

def cars = [
       new Car(make: 'Peugeot', model: '508'),
       new Car(make: 'Renault', model: 'Clio')
]       
def makes = cars*.make                                

assert makes == ['Peugeot', 'Renault'] 

Elvis Operator

displayName = user?.name ? user.name : 'White Rose'   
displayName = user?.name ?: 'White Rose'     

Left Shift Operator

def x = [1,2]

x << 3

assert x == [1,2,3]

GDK Collection Methods

def list = [1,2,3]

list.each { it ->
    println it
}

assert list.collect { it ->
    it * 2
} == [2,4,6]

assert list.sum { it ->
    it
} == 6

assert list.find { it ->
    it == 1
} == 1

assert list.findAll { it ->
    it < 3
} == [1,2]

Pretty Groovy, Eh?

Now...

Back To Spark...

Obligatory Hello World

import static spark.Spark.*;

public class HelloWorld {
    public static void main(String[] args) {
        get("/hello", (req, res) -> "Hello World");
    }
}

Java

Kotlin

import spark.kotlin.*

fun main(args: Array<String>) {
    val http: Http = ignite()

    http.get("/hello") {
        "Hello Spark Kotlin!"
    }
}

@recursivecodes

Let's Make It Groovy!

import static spark.Spark.*;

public class HelloWorld {
    public static void main(String[] args) {
        get("/hello", (req, res) -> "Hello World");
    }
}

Groovy

import static spark.Spark.*

class HelloWorld {
    static void main(String[] args) {
        get "/hello", { req, res -> "Hi!" }
    }
}

Java

@recursivecodes

Getting Started

Java (Maven)

<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-kotlin</artifactId>
    <version>1.0.0-alpha</version>
</dependency>

Kotlin (Maven)

<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-core</artifactId>
    <version>2.7.1</version>
</dependency>

@recursivecodes

Getting Started

Other Dependency Managers

// Java/Groovy - Gradle: 
compile "com.sparkjava:spark-core:2.7.1" 

// Kotlin - Gradle: 
compile "com.sparkjava:spark-kotlin:1.0.0-alpha" 

//Ivy:
<dependency 
    org="com.sparkjava" 
    name="spark-core" 
    rev="2.7.1" conf="build" />

//SBT: 
libraryDependencies += "com.sparkjava" % "spark-core" % "2.7.1" 

@recursivecodes

Getting Started

build.gradle for Groovy

group 'codes.recursive'
version '1.0-SNAPSHOT'

apply plugin: 'idea'
apply plugin: 'groovy'
apply plugin: 'java'

configurations {
    localGroovyConf
}
repositories {
    mavenCentral()
}

dependencies {
    localGroovyConf localGroovy()
    compile 'org.codehaus.groovy:groovy-all:2.3.11'
    compile 'com.sparkjava:spark-core:2.6'
}

task runServer(dependsOn: 'classes', type: JavaExec) {
    // put Groovy classes in src/main/groovy!
    classpath = sourceSets.main.runtimeClasspath
    main = 'Bootstrap'
}

@recursivecodes

Spark Application Structure

@recursivecodes

Skeleton App

@recursivecodes

The "Controller"

All Routes are declared in the application's main() method, but of course there are techniques to modularize your code and make it more manageable.

@recursivecodes

Routes

  • Verb
    • GET, POST, PUT, DELETE, HEAD, TRACE, CONNECT, OPTIONS
  • Path
    • /user/edit, /home/view
  • Callback
    • Lambda/Closure
      • Get data, Render view, etc

Routes consist of three parts:

@recursivecodes

Paths

Routes can be grouped into paths

@recursivecodes

path "/blog", {
    path "/post", {
        get "/show", { req, res -> return 'blog post'}
        post "/edit", { req, res -> return 'post saved'}
    }
}

http://sparkjava.com/documentation#path-groups

Route Parameters

Routes can contain named parameters

@recursivecodes

// matches "GET /hello/foo" and "GET /hello/bar"
// request.params(":name") is 'foo' or 'bar'

get "/hello/:name", { req, res ->
    return "Hello: ${req.params(':name')}"
}

Wildcard Parameters

@recursivecodes

// matches "GET /say/hello/to/world"
// request.splat()[0] is 'hello' and request.splat()[1] 'world'

get "/say/*/to/*", { req, res ->
    return "Number of splat parameters: ${request.splat().size()}"
}

Response Transformers

@recursivecodes

import com.google.gson.Gson;

public class JsonTransformer implements ResponseTransformer {

    private Gson gson = new Gson();

    @Override
    public String render(Object model) {
        return gson.toJson(model);
    }

}
get("/hello", "application/json", (request, response) -> {
    return new MyMessage("Hello World");
}, new JsonTransformer());

Response Transformers

@recursivecodes

Gson gson = new Gson();
get("/hello", (request, response) -> 
    new MyMessage("Hello World"), gson::toJson);

Using Java 8 Method References

Response Transformers

@recursivecodes

get "/json", {req, res ->
    res.type("application/json")
    JsonOutput.toJson([json: true, uncool: false])
}

Alternative:  Don't use a transformer, just return JSON

*If you really want to, create a ResponseTransformer that uses JsonOutput.toJson()

Request

@recursivecodes

request.attributes();             // the attributes list
request.attribute("foo");         // value of foo attribute
request.attribute("A", "V");      // sets value of attribute A to V
request.body();                   // request body sent by the client
request.bodyAsBytes();            // request body as bytes
request.contentLength();          // length of request body
request.contentType();            // content type of request.body
request.contextPath();            // the context path, e.g. "/hello"
request.cookies();                // request cookies sent by the client
request.headers();                // the HTTP header list
request.headers("BAR");           // value of BAR header
request.host();                   // the host, e.g. "example.com"
request.ip();                     // client IP address
request.params("foo");            // value of foo path parameter

//...and many more

The request parameter contains information and functionality related to the HTTP request

request.queryMap()

@recursivecodes

post "/qmap", { req, res ->
    def a = req.queryMap().get("user", "name").value()
    def b = req.queryMap().get("user").get("name").value()
    def c = req.queryMap("user").get("age").integerValue()
    def d = req.queryMap("user").toMap()

    JsonOutput.toJson(
            [
                    "req.queryMap().get('user', 'name').value()": a,
                    "req.queryMap().get('user').get('name').value()": b,
                    "req.queryMap('user').get('age').integerValue()": c,
                    "req.queryMap('user').toMap()": d,
            ]
    )
}

Group parameters to a map by their prefix. 

<form action="/qmap" method="POST">
    <input type="text" name="user[name]" />
    <input type="number" name="user[age]" />
</form>

Response

@recursivecodes

response.body();               // get response content
response.body("Hello");        // sets content to Hello
response.header("FOO", "bar"); // sets header FOO with value bar
response.raw();                // raw response handed in by Jetty
response.redirect("/example"); // browser redirect to /example
response.status();             // get the response status
response.status(401);          // set status code to 401
response.type();               // get the content type
response.type("text/xml");     // set content type to text/xml

The response parameter contains information and functionality related to the HTTP response

Cookies

@recursivecodes

request.cookies();                         // get map of all request cookies
request.cookie("foo");                     // access request cookie by name
response.cookie("foo", "bar");             // set cookie with a value
response.cookie("foo", "bar", 3600);       // set cookie with a max-age
response.cookie("foo", "bar", 3600, true); // secure cookie
response.removeCookie("foo");              // remove cookie

Sessions

@recursivecodes

request.session(true);                     // create and return session
request.session().attribute("user");       // Get session attribute 'user'
request.session().attribute("user","foo"); // Set session attribute 'user'
request.session().removeAttribute("user"); // Remove session attribute 'user'
request.session().attributes();            // Get all session attributes
request.session().id();                    // Get session id
request.session().isNew();                 // Check if session is new
request.session().raw();                   // Return servlet object

Halting

@recursivecodes

halt();                // halt 
halt(401);             // halt with status
halt("Body Message");  // halt with message
halt(401, "Go away!"); // halt with status and messageCopy

Redirects

@recursivecodes

response.redirect("/bar");
response.redirect("/bar", 301);

// redirect a GET to "/fromPath" to "/toPath"
redirect.get("/fromPath", "/toPath");

// redirect a POST to "/fromPath" to "/toPath", with status 303
redirect.post("/fromPath", "/toPath", Redirect.Status.SEE_OTHER);

// redirect any request to "/fromPath" to "/toPath" with status 301
redirect.any("/fromPath", "/toPath", Redirect.Status.MOVED_PERMANENTLY);

Filters

@recursivecodes

  • before
  • after
  • afterAfter
  • supports pattern matching on path
before "/*", { req, res ->
    def authenticated = false
    if( req.cookie('isSuperCool') == true ) {
        authenticated = true
    }
    if ( !authenticated ) {
        println('You are not welcome here!')
        // res.redirect('/login')
        // halt(401, "Unauthorized")
    }
}

Static File Filters:  https://goo.gl/PUvZH4

Error Handling

@recursivecodes

// Using string/html
notFound("<html><body><h1>Custom 404 handling</h1></body></html>")

// Using Route
notFound { req, res -> 
    res.type("application/json");
    return "{\"message\":\"Custom 404\"}";
}

// 500 error handling
internalServerError("<html><body><h1>Custom 500 handling</h1></body></html>")

// Using Route
internalServerError {req, res) -> 
    res.type("application/json");
    return "{\"message\":\"Custom 500 handling\"}";
}

// Exception Mapping
exception(YourCustomException.class, (exception, request, response) -> {
    // Handle the exception here
});

Static Files

@recursivecodes

staticFileLocation("/public")

// You can also assign an external folder 
// (a folder not in the classpath) to serve 
// static files

staticFiles.externalLocation(System.getProperty("java.io.tmpdir")) 

staticFiles.expireTime(600); // ten minutes

staticFiles.header("Key-1", "Value-1");
staticFiles.header("Key-2", "Value-2");
staticFiles.header("Key-3", "Value-3");

You can assign a folder in the classpath serving static files with the staticFiles.location() method. Note that the public directory name is not included in the URL.

Views And Templates

  • Velocity
  • Freemarker
  • Mustache
  • Handlebars
  • Jade
  • Thymeleaf
  • Pebble
  • Water
  • jTwig
  • Jinjava
  • Jetbrick

Spark has community-provided wrappers for a lot of popular template engines:

 

@recursivecodes

Views And Templates

  • Angular
  • Vue
  • React
  • Etc...

But obviously a microservice framework is probably better suited to be an API gateway for:

@recursivecodes

Views And Templates

Create engine, call render():

static void main(String[] args) {

    ThymeleafTemplateEngine engine = new ThymeleafTemplateEngine()
    engine.templateEngine.addDialect(new LayoutDialect())

    get "/thymeleaf", { req, res ->
        def model = [users: personService.list()]
        return engine.render(new ModelAndView(model, "thymeleaf"))
    }
}

@recursivecodes

Embedded Web Server

Standalone Spark runs on an embedded Jetty web server.

port(8080);

// HTTPS/SSL
secure(keystoreFilePath, keystorePassword, truststoreFilePath, truststorePassword);

// thread pool
int maxThreads = 8;
int minThreads = 2;
int timeOutMillis = 30000;
threadPool(maxThreads, minThreads, timeOutMillis);

@recursivecodes

Spark With Embedded Jetty Is Lightweight Enough To Run on a Raspberry Pi

Embedded Web Server

Jetty Supports WebSockets!!

// Bootstrap.groovy:

static List<Session> users = [];

webSocket("/socket", WebSocketHandler.class)

static void broadcastMessage(String sender, Object message) {
    users.findAll{ Session it -> it.isOpen() }.each { Session it ->
        try {
            def msg = JsonOutput.toJson([message: message])
            it.getRemote().sendString(String.valueOf(msg))
        } catch (Exception e) {
            e.printStackTrace()
        }
    }
}

@recursivecodes

Embedded Web Server

Jetty Supports WebSockets!!

import org.eclipse.jetty.websocket.api.*
import org.eclipse.jetty.websocket.api.annotations.*

@WebSocket
class WebSocketHandler {
    @OnWebSocketConnect
    void onConnect(Session user) throws Exception {
        Bootstrap.users << user
        Bootstrap.broadcastMessage("Server",
                [
                        message: "connection established from ${user.remoteAddress}"
                ]
        )
    }
    @OnWebSocketClose
    void onClose(Session user, int statusCode, String reason) {
        Bootstrap.broadcastMessage("Server",
                [
                        message: "connection disconnected from ${user.remoteAddress}"
                ]
        )
        Bootstrap.users.remove(user)
    }
    @OnWebSocketMessage
    void onMessage(Session user, String message) {
        Bootstrap.broadcastMessage(user, [message: message])
    }

}

@recursivecodes

Handling File Uploads

@recursivecodes

Data Access With Morphia And MongoDB

@recursivecodes

Using Thymeleaf Views

@recursivecodes

Documenting Your API With Swagger

@recursivecodes

Inspired By: https://goo.gl/EYwq1c

// new dependencies

dependencies {
    localGroovyConf localGroovy()
    compile 'org.codehaus.groovy:groovy-all:2.5.0-beta-1'
    compile 'com.sparkjava:spark-core:2.7.0'
    compile 'org.slf4j:slf4j-simple:1.7.21'
    compile 'org.reflections:reflections:0.9.10'
    compile 'io.swagger:swagger-core:1.5.8'
    compile 'io.swagger:swagger-jaxrs:1.5.8'
    compile 'javax.ws.rs:javax.ws.rs-api:2.0.1'
}

Swagger Parser

@recursivecodes

  • Uses Reflection
  • Scans Package
  • Reads Annotations
  • Generates JSON

Not 'out-of-the-box'

@recursivecodes

Swagger annotations are applied per class/method so each route must correspond to a class and method

Not As Bad As It Sounds

@recursivecodes

static void main(String[] args) {
    staticFileLocation("/public")
    staticFiles.expireTime(10)
    port(9001)
    path "/user", {
        get "/hello", { req, res -> 
            return User.hello(req, res)
        }
        get "/goodbye", { req, res -> 
            return User.goodbye(req, res)
        }
    }
    get "/swagger", { req, res -> 
        return SwaggerParser.getSwaggerJson('codes.recursive') 
    }
}

Not As Bad As It Sounds

@recursivecodes

@Api(value = "/user", tags = ["User"], description = "User API")
@Path('/user')
class User {

    @Path('/hello')
    @ApiOperation(
        value = 'Says hello to you', 
        nickname='hello', 
        httpMethod='GET', 
        produces='application/json')
    @ApiImplicitParams([
        @ApiImplicitParam( 
            name = 'name', 
            paramType = 'query', 
            required = true, 
            dataType = 'string' )
    ])
    static def hello(@ApiParam(hidden=true) Request req, @ApiParam(hidden=true) Response res){
        return JsonOutput.toJson([message: "Hello, ${req.queryParams('name')}"])
    }

    @Path('/goodbye')
    @ApiOperation(value = 'Says goodbye to you', nickname='goodbye', httpMethod='GET', produces='application/json')
    @ApiImplicitParams([
        @ApiImplicitParam( name = 'name', paramType = 'query', required = true, dataType = 'string' )
    ])
    static def goodbye(@ApiParam(hidden=true) Request req, @ApiParam(hidden=true) Response res){
        return JsonOutput.toJson([message: "Goodbye, ${req.queryParams('name')}"])
    }

}

Now What???

When Should I Use It?

  • Microservices 
    • The Java Version Of Node/Express
  • Monoliths With Very Light View Requirements
  • Embedded Devices?
  • Need For Simple Websocket Support

When Shouldn't I Use It?

  • Applications With Heavy Data Access Requirements
    • Use Grails with GORM
  • Monoliths With Extensive Views
    • Use Grails with GSPs

Resources

Thank You!