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
- Lambda/Closure
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