Table of Contents
- Chapter One: Introduction to Javalin.
- Chapter Two : “Hello Javelin”
- Chapter Three: Creating a Google Docs clone with WebSockets using Javalin
- Chapter Four: Creating a simple chat-app with WebSockets
- Chapter Five: Working with HTML forms and a Java backend
- Chapter Six: A comparison with Spark
- Chapter 7: Javalin and Docker
- Chapter 8: Sending email with Javalin
- Chapter 10: Authentication with Javalin
Chapter One: Introduction to Javalin.
What is Javelin?
Javalin is a web framework runs on top of Jetty, one of the most used and stable web-servers on the JVM. You can configure the Jetty server fully, so you can easily get SSL and HTTP2 and everything else that Jetty has to offer. You can use Javelin with either Java
or Kotlin
languages
Javalin started as a fork of the Java and Kotlin web framework Spark, but quickly turned into a ground-up rewrite influenced by koa.js. Both of these web frameworks are inspired by the modern micro web framework grandfather: Sinatra, so if you’re coming from Ruby then Javalin shouldn’t feel too unfamiliar.
Philosophy
Like Sinatra, Javalin is not aiming to be a full web framework, but rather
just a lightweight REST API library (or a micro framework, if you must). There is no concept of MVC,
but there is support for template engines, WebSockets, and static file serving for convenience.
This allows you to use Javalin for both creating your RESTful API backend, as well as serving
an index.html
with static resources (in case you’re creating an SPA). This is practical
if you don’t want to deploy an apache or nginx server in addition to your Javalin service.
If you wish to use Javalin to create a more traditional website instead of a REST APIs,
there are several template engine wrappers available for a quick and easy setup.
API design
All the methods on the Javalin instance return this
, making the API fully fluent.
This will let you create a declarative and predictive REST API,
that will be very easy to reason about for new developers joining your project.
Java and Kotlin interoperability
Javalin is both a Kotlin web framework and a Java web framework, meaning the API is being developed with focus on great interoperability between the two languages. The library itself is written primarily in Kotlin, but has a few core classes written in Java to achieve the best interoperability between the two languages. Javalin is intended as a “foot in the door” to Kotlin development for companies that already write a lot of Java.
When moving a Javalin project from Java to Kotlin, you shouldn’t need to learn a new way of doing things. To maintain this consistent API for both languages is an important goal of the project.
Sailient features of Javalin
Simple
Unlike other Java and Kotlin web frameworks, Javalin has very few concepts that you need to learn. You never have to extend a class and you rarely have to implement an interface.
Lightweight
Javalin is just a few thousand lines of code on top of Jetty, which means its performance is almost equivalent to pure Jetty. It also means it’s very easy to reason about the source code.
Active
A new version of Javalin has been released twice a month (on average) since the first version. Don’t worry though, every version is backwards compatible. PRs and issues are reviewed swiftly, normally every week.
Interoperable
Other Java and Kotlin web frameworks usually offer separate version for each language. Javalin is being developed with interoperability in mind, so apps are built the same way in both Java and Kotlin.
Flexible
Javalin is designed to be simple and blocking, as this is the easiest programming model to reason about. However, if you set a Future as a result, Javalin switches into asynchronous mode.
Why should I use Javelin over Spring Boot or Vert.x?
Javalin is a microservice framework with a built in app server ( making it easy to produce fat jars). Unlike Vert.x this has a simple programming model and is easier to code, debug and maintain. Spring Boot is “heavy” framework and typically requires a lot of setup for simple API app. The binaries produced are also fatter.
Javalin helps you write API based applications easily with minimal overhead and gets the job done. You also have full access to the power of JVM threading and asynchronous calls. It is, of course, possible to use all existing Java libraries, thus making Javalin an easy fit in your standard Java development ecosystem.
Can I write asynchronous code with Javalin?
Yes you can. Just set a CompletableFuture<String>
or CompletableFuture<InputStream>
as your result:
1
import
io.javalin.Javalin
2
3
fun
main
(
args
:
Array
<
String
>)
{
4
val
app
=
Javalin
.
start
(
7000
)
5
app
.
get
(
"/"
)
{
ctx
->
ctx
.
result
(
getFuture
())
}
6
}
7
8
// hopefully your future is less pointless than this:
9
private
fun
getFuture
()
=
CompletableFuture
<
String
>().
app
\
10
ly
{
11
Executors
.
newSingleThreadScheduledExecutor
().
schedule
\
12
({
this
.
complete
(
"Hello World!"
)
},
1
,
TimeUnit
.
SECONDS
)
13
}
How “Fast” is Javelin?
This really depends on your application. Javelin apps will be “as fast” as a standard Java application
How easy is Javelin to Learn?
Javalin is a micro-framework which really does not require any additional knowledge if you are already an existing Java developer who has developed a web app before. Please note that this is not an introductory book and there are several excellent online resources and books available which teach Java and Java Web Programming basics. It is recommended to familiarize oneself with these concepts before proceeding.
Summary
Javalin is a fascinating micro-framework. I really see it as a sort of NodeJS of the JVM. Something that is needed with the abundance of opinionated and heavy-weight frameworks out there.
Chapter Two : “Hello Javelin”
Setting up your IDE
Provide instructions and a screenshot to download IntelliJ Idea.
Creating a project
IntelliJ IDEA lets you create a Maven project or add a Maven support to any existing project.
- Launch the New Project wizard. If no project is currently opened in IntelliJ IDEA, click Create New Project on the Welcome screen: Otherwise, select File | New | Project from the main menu.
- Select Maven from the options on the left.
- Specify project’s SDK (JDK) or use a default one and an archetype if you want to use a predefined project template (configure your own archetype by clicking Add Archetype).
- Click Next.
- On the next page of the wizard, specify the following Maven basic elements that are added to the pom.xml file:
- GroupId - a package of a new project.
- ArtifactId - a name of your project.
- Version - a version of a new project. By default, this field is specified automatically.
- Click Next.
- If you are creating a project using a Maven archetype, IntelliJ IDEA displays Maven settings that you can use to set the Maven home directory and Maven repositories. Also, you can check the archetype properties. Click Next. Specify the name and location settings. Click Finish.
Setting up Maven
You should have the following code in your pom.xml:
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project
xmlns=
"http://maven.apache.org/POM/4.0.0"
3
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-inst\
4
ance"
5
xsi:schemaLocation=
"http://maven.apache.org/POM/\
6
4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
>
7
<modelVersion>
4.0.0</modelVersion>
8
<groupId>
com</groupId>
9
<artifactId>
manish</artifactId>
10
<version>
1.0-SNAPSHOT</version>
11
<build>
12
<plugins>
13
<plugin>
14
<groupId>
org.apache.maven.plugins<
/groupI\
15
d>
16
<artifactId>
maven-compiler-plugin<
/artifa\
17
ctId>
18
<configuration>
19
<source>
8</source>
20
<target>
8</target>
21
</configuration>
22
</plugin>
23
</plugins>
24
</build>
25
<dependencies>
26
<dependency>
27
<groupId>
io.javalin</groupId>
28
<artifactId>
javalin</artifactId>
29
<version>
1.7.0</version>
30
</dependency>
31
</dependencies>
32
</project>
TIP: To download source code of Javalin:
mvn dependency:sources
A simple “Hello Javelin” app
Create a class called HelloWorld
and write the following code in it:
1
import
io.javalin.Javalin
;
2
public
class
HelloWorld
{
3
public
static
void
main
(
String
[]
args
)
{
4
Javalin
app
=
Javalin
.
start
(
7000
);
5
app
.
get
(
"/"
,
ctx
->
ctx
.
result
(
"Hello Javelin"
));
6
}
7
}
A quick overview of the “Hello Javelin” App
Let us now go through the application describing what each component does in detail.
1
Javalin
app
=
Javalin
.
start
(
7000
);
The code above starts the application on port 7000. Let us see what internally happens when we start a Javalin app.
Javalin app internals: Startup
1
public
Javalin
start
()
{
2
if
(!
started
)
{
3
if
(!
hideBanner
)
{
4
log
.
info
(
Util
.
INSTANCE
.
javalinBanner
());
5
}
6
Util
.
INSTANCE
.
printHelpfulMessageIfLoggerIsMissin
\
7
g
();
8
Util
.
INSTANCE
.
setNoServerHasBeenStarted
(
false
);
9
eventManager
.
fireEvent
(
EventType
.
SERVER_STARTING
,
\
10
this
);
11
try
{
12
embeddedServer
=
embeddedServerFactory
.
create
\
13
(
new
JavalinServlet
(
14
contextPath
,
15
pathMatcher
,
16
exceptionMapper
,
17
errorMapper
,
18
jettyWsHandlers
,
19
javalinWsHandlers
,
20
logLevel
,
21
dynamicGzipEnabled
,
22
defaultContentType
,
23
defaultCharacterEncoding
,
24
maxRequestCacheBodySize
25
),
staticFileConfig
);
26
log
.
info
(
"Starting Javalin ..."
);
27
port
=
embeddedServer
.
start
(
port
);
28
log
.
info
(
"Javalin has started \\o/"
);
29
started
=
true
;
30
eventManager
.
fireEvent
(
EventType
.
SERVER_START
\
31
ED
,
this
);
32
}
catch
(
Exception
e
)
{
33
log
.
error
(
"Failed to start Javalin"
,
e
);
34
if
(
e
instanceof
BindException
&&
e
.
getMessag
\
35
e
()
!=
null
)
{
36
if
(
e
.
getMessage
().
toLowerCase
().
contains
\
37
(
"in use"
))
{
38
log
.
error
(
"Port already in use. Make \
39
sure no other process is using port "
+
port
+
" and try \
40
again."
);
41
}
else
if
(
e
.
getMessage
().
toLowerCase
().
c
\
42
ontains
(
"permission denied"
))
{
43
log
.
error
(
"Port 1-1023 require elevat\
44
ed privileges (process must be started by admin)."
);
45
}
46
}
47
eventManager
.
fireEvent
(
EventType
.
SERVER_START
\
48
_FAILED
,
this
);
49
}
50
}
51
return
this
;
52
}
Javalin app internals:
The second line of the Javalin app reads:
1
app
.
get
(
"/"
,
ctx
->
ctx
.
result
(
"Hello Javelin"
));
The Context instance.
The ctx
represents an instance of the Context
object, which contains everything you need to handle an http
request.
It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.
Context methods: A reference
1
// REQUEST METHODS
2
ctx
.
request
();
3
// get underlying HttpServletRequest
4
ctx
.
anyFormParamNull
(
"k1"
,
"k2"
);
5
// returns true if any form-param is null
6
ctx
.
anyQueryParamNull
(
"k1"
,
"k2"
);
7
// returns true if any query-param is null
8
ctx
.
body
();
9
// get the request body as string
10
ctx
.
bodyAsBytes
();
11
// get the request body as byte-array
12
ctx
.
bodyAsClass
(
clazz
);
13
// convert json body to object
14
ctx
.
formParam
(
"key"
);
15
// get form param
16
ctx
.
formParams
(
"key"
);
17
// get form param with multiple values
18
ctx
.
formParamMap
();
19
// get all form param key/values as map
20
ctx
.
param
(
"key"
);
21
// get a path-parameter, ex "/:id" -> param("id")
22
ctx
.
paramMap
();
23
// get all param key/values as map
24
ctx
.
splat
(
0
);
25
// get splat by nr, ex "/*" -> splat(0)
26
ctx
.
splats
();
27
// get array of splat-values
28
ctx
.
attribute
(
"key"
,
"value"
);
29
// set a request attribute
30
ctx
.
attribute
(
"key"
);
31
// get a request attribute
32
ctx
.
attributeMap
();
33
// get all attribute key/values as map
34
ctx
.
basicAuthCredentials
()
35
// get username and password used for basic-auth
36
ctx
.
contentLength
();
37
// get request content length
38
ctx
.
contentType
();
39
// get request content type
40
ctx
.
cookie
(
"key"
);
41
// get cookie by name
42
ctx
.
cookieMap
();
43
// get all cookie key/values as map
44
ctx
.
header
(
"key"
);
45
// get a header
46
ctx
.
headerMap
();
47
// get all header key/values as map
48
ctx
.
host
();
49
// get request host
50
ctx
.
ip
();
51
// get request up
52
ctx
.
isMultipart
();
53
// check if request is multipart
54
ctx
.
mapFormParams
(
"k1"
,
"k2"
);
55
// map form params to their values, returns null if any f\
56
orm
param
is
missing
57
ctx
.
mapQueryParams
(
"k1"
,
"k2"
);
58
// map query params to their values, returns null if any \
59
query
param
is
missing
60
ctx
.
matchedPath
();
61
// get matched path, ex "/path/:param"
62
ctx
.
next
();
63
// pass the request to the next handler
64
ctx
.
path
();
65
// get request path
66
ctx
.
port
();
67
// get request port
68
ctx
.
protocol
();
69
// get request protocol
70
ctx
.
queryParam
(
"key"
);
71
// get query param
72
ctx
.
queryParams
(
"key"
);
73
// get query param with multiple values
74
ctx
.
queryParamMap
();
75
// get all query param key/values as map
76
ctx
.
queryString
();
77
// get request query string
78
ctx
.
method
();
79
// get request method
80
ctx
.
scheme
();
81
// get request scheme
82
ctx
.
sessionAttribute
(
"foo"
,
"bar"
);
83
// set session-attribute "foo" to "bar"
84
ctx
.
sessionAttribute
(
"foo"
);
85
// get session-attribute "foo"
86
ctx
.
sessionAttributeMap
();
87
// get all session attributes as map
88
ctx
.
uploadedFile
(
"key"
);
89
// get file from multipart form
90
ctx
.
uploadedFiles
(
"key"
);
91
// get files from multipart form
92
ctx
.
uri
();
93
// get request uri
94
ctx
.
url
();
95
// get request url
96
ctx
.
userAgent
();
97
// get request user agent
98
99
// RESPONSE methods
100
101
ctx
.
response
();
102
// get underlying HttpServletResponse
103
ctx
.
result
(
"result"
);
104
// set result (string)
105
ctx
.
result
(
inputStream
);
106
// set result (stream)
107
ctx
.
result
(
future
);
108
// set result (future)
109
ctx
.
resultString
();
110
// get response result (string)
111
ctx
.
resultStream
();
112
// get response result (stream)
113
ctx
.
resultFuture
();
114
// get response result (future)
115
ctx
.
charset
(
"charset"
);
116
// set response character encoding
117
ctx
.
header
(
"key"
,
"value"
);
118
// set response header
119
ctx
.
html
(
"body html"
);
120
// set result and html content type
121
ctx
.
json
(
object
);
122
// set result with object-as-json
123
ctx
.
redirect
(
"/location"
);
124
// redirect to location
125
ctx
.
redirect
(
"/location"
,
302
);
126
// redirect to location with code
127
ctx
.
status
();
128
// get response status
129
ctx
.
status
(
404
);
130
// set response status
131
ctx
.
cookie
(
"key"
,
"value"
);
132
// set cookie with key and value
133
ctx
.
cookie
(
"key"
,
"value"
,
0
);
134
// set cookie with key, value, and maxage
135
ctx
.
cookie
(
cookieBuilder
);
136
// set cookie using cookiebuilder
137
ctx
.
removeCookie
(
"key"
);
138
// remove cookie by key
139
ctx
.
removeCookie
(
"/path"
,
"key"
);
140
// remove cookie by path and key
Server Startup and Lifecyle management
To start and stop the server, use the appropriately named start()
and stop()
methods.
1
Javalin
app
=
Javalin
.
create
()
2
.
start
()
// start server (sync/blocking)
3
.
stop
()
// stop server (sync/blocking)
1
Javalin
app
=
Javalin
.
start
(
7000
);
Server setup: Available configurations
1
Javalin
.
create
()
// create has to be called first
2
.
contextPath
(
"/context-path"
)
3
// set a context path (default is "/")
4
.
dontIgnoreTrailingSlashes
()
5
// treat '/test' and '/test/' as different URLs
6
.
defaultContentType
(
string
)
7
// set a default content-type for responses
8
.
defaultCharacterEncoding
(
string
)
9
// set a default character-encoding for responses
10
.
disableStartupBanner
()
11
// remove the javalin startup banner from logs
12
.
embeddedServer
(
...
)
13
// see section below
14
.
enableCorsForOrigin
(
"origin"
)
15
// enables cors for the specified origin(s)
16
.
enableDynamicGzip
()
17
// gzip response (if client accepts gzip and response\
18
is
more
than
1500
bytes
)
19
.
enableRouteOverview
(
"/path"
)
20
// render a HTML page showing all mapped routes
21
.
enableStandardRequestLogging
()
22
// does requestLogLevel(LogLevel.STANDARD)
23
.
enableStaticFiles
(
"/public"
)
24
// enable static files (opt. second param Location.CL\
25
ASSPATH
/
Location
.
EXTERNAL
)
26
.
maxBodySizeForRequestCache
(
long
)
27
// set max body size for request cache
28
.
port
(
port
)
29
// set the port
30
.
start
();
31
// start has to be called last
Custom Server
1
app
.
embeddedServer
(
new
EmbeddedJettyFactory
(()
->
{
2
Server
server
=
new
Server
();
3
// do whatever you want here
4
return
server
;
5
}));
Custom jetty handlers
You can configure your embedded jetty-server with a handler-chain, and Javalin will attach it’s own handlers to the end of this chain.
1
StatisticsHandler
statisticsHandler
=
new
StatisticsHandl
\
2
er
();
3
4
Javalin
.
create
()
5
.
embeddedServer
(
new
EmbeddedJettyFactory
(()
->
{
6
Server
server
=
new
Server
();
7
server
.
setHandler
(
statisticsHandler
);
8
return
server
;
9
}))
10
.
start
();
Implementing a custom server with SSL
Implementing a custom server with SSL enabled is easy in Javalin, but not straightforward. It may require hunting around in the documentation. For your reference, here is a complete working example with SSL
1
import
io.javalin.Javalin
;
2
import
org.eclipse.jetty.server.Connector
;
3
import
org.eclipse.jetty.server.Server
;
4
import
org.eclipse.jetty.server.ServerConnector
;
5
import
org.eclipse.jetty.util.ssl.SslContextFactory
;
6
7
public
class
HelloWorldSecure
{
8
9
// This is a very basic example, a better one can be \
10
found
at
:
11
// https://github.com/eclipse/jetty.project/blob/jett\
12
y
-
9.4
.
x
/
examples
/
embedded
/
src
/
main
/
java
/
org
/
eclipse
/
jetty
\
13
/
embedded
/
LikeJettyXml
.
java
#
L139
-
L163
14
public
static
void
main
(
String
[]
args
)
{
15
Javalin
.
create
()
16
.
server
(()
->
{
17
Server
server
=
new
Server
();
18
ServerConnector
sslConnector
=
new
Server
\
19
Connector
(
server
,
getSslContextFactory
());
20
sslConnector
.
setPort
(
443
);
21
ServerConnector
connector
=
new
ServerCon
\
22
nector
(
server
);
23
connector
.
setPort
(
80
);
24
server
.
setConnectors
(
new
Connector
[]{
sslC
\
25
onnector
,
connector
});
26
return
server
;
27
})
28
.
start
()
29
.
get
(
"/"
,
ctx
->
ctx
.
result
(
"Hello World"
));
\
30
// valid endpoint for both connectors
31
}
32
33
private
static
SslContextFactory
getSslContextFactory
\
34
()
{
35
SslContextFactory
sslContextFactory
=
new
SslCont
\
36
extFactory
();
37
sslContextFactory
.
setKeyStorePath
(
HelloWorldSecur
\
38
e
.
class
.
getResource
(
"/keystore.jks"
).
toExternalForm
());
39
sslContextFactory
.
setKeyStorePassword
(
"password"
);
40
return
sslContextFactory
;
41
}
42
}
Implementing a custom server with HTTP/2
The following is a sample server implemented with HTTP2. There is no straight-forward example easily, please use the following code below:
1
import
io.javalin.Javalin
;
2
import
io.javalin.embeddedserver.jetty.EmbeddedJettyFacto
\
3
ry
;
4
import
org.eclipse.jetty.alpn.ALPN
;
5
import
org.eclipse.jetty.alpn.server.ALPNServerConnection
\
6
Factory
;
7
import
org.eclipse.jetty.http2.HTTP2Cipher
;
8
import
org.eclipse.jetty.http2.server.HTTP2ServerConnecti
\
9
onFactory
;
10
import
org.eclipse.jetty.server.HttpConfiguration
;
11
import
org.eclipse.jetty.server.HttpConnectionFactory
;
12
import
org.eclipse.jetty.server.SecureRequestCustomizer
;
13
import
org.eclipse.jetty.server.Server
;
14
import
org.eclipse.jetty.server.ServerConnector
;
15
import
org.eclipse.jetty.server.SslConnectionFactory
;
16
import
org.eclipse.jetty.util.ssl.SslContextFactory
;
17
18
public
class
Main
{
19
20
public
static
void
main
(
String
[]
args
)
{
21
22
Javalin
app
=
Javalin
.
create
()
23
.
embeddedServer
(
createHttp2Server
())
24
.
enableStaticFiles
(
"/public"
)
25
.
start
();
26
27
app
.
get
(
"/"
,
ctx
->
ctx
.
result
(
"Hello World"
));
28
29
}
30
31
private
static
EmbeddedJettyFactory
createHttp2Server
\
32
()
{
33
return
new
EmbeddedJettyFactory
(()
->
{
34
Server
server
=
new
Server
();
35
36
ServerConnector
connector
=
new
ServerConnect
\
37
or
(
server
);
38
connector
.
setPort
(
8080
);
39
server
.
addConnector
(
connector
);
40
41
// HTTP Configuration
42
HttpConfiguration
httpConfig
=
new
HttpConfig
\
43
uration
();
44
httpConfig
.
setSendServerVersion
(
false
);
45
httpConfig
.
setSecureScheme
(
"https"
);
46
httpConfig
.
setSecurePort
(
8443
);
47
48
// SSL Context Factory for HTTPS and HTTP/2
49
SslContextFactory
sslContextFactory
=
new
Ssl
\
50
ContextFactory
();
51
sslContextFactory
.
setKeyStorePath
(
Main
.
class
.
\
52
getResource
(
"/keystore.jks"
).
toExternalForm
());
53
// replace with your real keystore
54
sslContextFactory
.
setKeyStorePassword
(
"passwo\
55
rd"
);
56
// replace with your real password
57
sslContextFactory
.
setCipherComparator
(
HTTP2Ci
\
58
pher
.
COMPARATOR
);
59
sslContextFactory
.
setProvider
(
"Conscrypt"
);
60
61
// HTTPS Configuration
62
HttpConfiguration
httpsConfig
=
new
HttpConfi
\
63
guration
(
httpConfig
);
64
httpsConfig
.
addCustomizer
(
new
SecureRequestCu
\
65
stomizer
());
66
67
// HTTP/2 Connection Factory
68
HTTP2ServerConnectionFactory
h2
=
new
HTTP2Se
\
69
rverConnectionFactory
(
httpsConfig
);
70
ALPNServerConnectionFactory
alpn
=
new
ALPNSe
\
71
rverConnectionFactory
();
72
alpn
.
setDefaultProtocol
(
"h2"
);
73
74
// SSL Connection Factory
75
SslConnectionFactory
ssl
=
new
SslConnectionF
\
76
actory
(
sslContextFactory
,
alpn
.
getProtocol
());
77
78
// HTTP/2 Connector
79
ServerConnector
http2Connector
=
new
ServerCo
\
80
nnector
(
server
,
ssl
,
alpn
,
h2
,
new
HttpConnectionFactory
(
\
81
httpsConfig
));
82
http2Connector
.
setPort
(
8443
);
83
server
.
addConnector
(
http2Connector
);
84
85
return
server
;
86
});
87
}
88
}
Note that you will have to generate a keystore locally using: keytool -genkey -alias mydomain -keyalg RSA -keystore keystore.jks -keysize 2048
Context Extensions:
Context extensions give Java developers a way of extending the Context object.
One of the most popular features of Kotlin is extension functions. When working with an object you don’t own in Java, you often end up making MyUtil.action(object, ...)
. If you, for example, want to serialize an object and set it as the result on the Context, you might do:
1
app.get("/", ctx -> MyMapperUtil.serialize(ctx, myMapper,\
2
myObject));
With context extensions you can add custom extensions on the context:
1
app.get("/", ctx -> ctx.use(MyMapper.class).serialize(obj\
2
ect)); // use MyMapper to serialize object
Context extensions have to be added before you can use them, this would typically be done in the first before filter of your app:
1
app.before(ctx -> ctx.register(MyMapper.class, new MyMapp\
2
er(ctx, otherDependency));
AccessManager for Authentication and Authorization
Javalin has a functional interface AccessManager
, which let’s you set per-endpoint authentication and/or authorization. It’s common to use before-handlers for this, but per-endpoint security handlers give you much more explicit and readable code. You can implement your access-manager however you want, but here is an example implementation:
1
// Set the access-manager that Javalin should use
2
app
.
accessManager
((
handler
,
ctx
,
permittedRoles
)
->
{
3
MyRole
userRole
=
getUserRole
(
ctx
);
4
if
(
permittedRoles
.
contains
(
userRole
))
{
5
handler
.
handle
(
ctx
);
6
}
else
{
7
ctx
.
status
(
401
).
result
(
"Unauthorized"
);
8
}
9
});
10
11
Role
getUserRole
(
Context
ctx
)
{
12
// determine user role based on request
13
// typically done by inspecting headers
14
}
15
16
enum
MyRole
implements
Role
{
17
ANYONE
,
ROLE_ONE
,
ROLE_TWO
,
ROLE_THREE
;
18
}
19
20
app
.
routes
(()
->
{
21
get
(
"/un-secured"
,
22
ctx
->
ctx
.
result
(
"Hello"
),
roles
(
ANYONE
));
23
get
(
"/secured"
,
24
ctx
->
ctx
.
result
(
"Hello"
),
roles
(
ROLE_ONE
));
25
});
Exception Mapping
All handlers (before, endpoint, after) can throw Exception
(and any subclass of Exception
) The app.exception()
method gives you a way of handling these exceptions:
1
app.exception(NullPointerException.class, (e, ctx) -> {
2
// handle nullpointers here
3
});
4
5
app.exception(Exception.class, (e, ctx) -> {
6
// handle general exceptions here
7
// will not trigger if more specific exception-mapper\
8
found
9
});
Javalin has a HaltException
which is handled before other exceptions. It can be used to short-circuit the request-lifecycle. If you throw a HaltException
in a before-handler, no endpoint-handler will fire. When throwing a HaltException
you can include a status code, a message, or both:
1
throw new HaltException(); // (status\
2
: 200, message: "Execution halted")
3
throw new HaltException(401); // (status\
4
: 401, message: "Execution halted")
5
throw new HaltException("My message"); // (status\
6
: 200, message: "My message")
7
throw new HaltException(401, "Unauthorized"); // (status\
8
: 401, message: "Unauthorized")
Error Mapping
Error mapping is similar to exception mapping, but it operates on HTTP status codes instead of Exceptions:
1
app.error(404, ctx -> {
2
ctx.result("Generic 404 message")
3
});
It can make sense to use them together:
1
app.exception(FileNotFoundException.class, (e, ctx) -> {
2
ctx.status(404);
3
}).error(404, ctx -> {
4
ctx.result("Generic 404 message")
5
});
WebSockets
Javalin has a very intuitive way of handling WebSockets, similar to most node frameworks:
1
app
.
ws
(
"/websocket/:path"
,
ws
-
>
{
2
ws.onConnect(session
->
System.out.println("Connected\
3
"))
;
4
ws.onMessage((session,
message)
->
{
5
System.out.println("
Received
:
" + message);
6
session.getRemote().sendString("
Echo
:
" + message\
7
);
8
});
9
ws.onClose((session, statusCode, reason) -> System.ou\
10
t.println("
Closed
"));
11
ws.onError((session, throwable) -> System.out.println\
12
("
Errored
"
));
13
}
);
The WsSession
object wraps Jetty’s Session
and adds the following methods:
1
session.send("message")
2
// send a message to session remote (the ws client)
3
session.queryString()
4
// get query-string from upgrade-request
5
session.queryParam("key")
6
// get query-param from upgrade-request
7
session.queryParams("key")
8
// get query-params from upgrade-request
9
session.queryParamMap()
10
// get query-param-map from upgrade-request
11
session.mapQueryParams("k1", "k2")
12
// map query-params to values (only useful in kotlin)
13
session.anyQueryParamNull("k1", "k2")
14
// check if any query-param from upgrade-request is null
15
session.param("key")
16
// get a path-parameter, ex "/:id" -> param("id")
17
session.paramMap()
18
// get all param key/values as map
19
session.header("key")
20
// get a header
21
session.headerMap()
22
// get all header key/values as map
23
session.host()
24
// get request host
Lifecycle events
Javalin has five lifecycle events: SERVER_STARTING
, SERVER_STARTED
, SERVER_START_FAILED
, SERVER_STOPPING
and SERVER_STOPPED
. The snippet below shows all of them in action:
1
Javalin app = Javalin.create()
2
.event(EventType.SERVER_STARTING, e -> { ... })
3
.event(EventType.SERVER_STARTED, e -> { ... })
4
.event(EventType.SERVER_START_FAILED, e -> { ... })
5
.event(EventType.SERVER_STOPPING, e -> { ... })
6
.event(EventType.SERVER_STOPPED, e -> { ... });
7
8
app.start(); // SERVER_STARTING -> (SERVER_STARTED || SER\
9
VER_START_FAILED)
10
app.stop(); // SERVER_STOPPING -> SERVER_STOPPED
Adding a logger:
If you’re reading this, you’ve probably seen the following message while running Javalin:
1
SLF4J
:
Failed
to
load
class
"org.slf4j.impl.StaticLoggerB\
2
inder"
.
3
SLF4J
:
Defaulting
to
no
-
operation
(
NOP
)
logger
implementa
\
4
tion
5
SLF4J
:
See
http
://
www
.
slf4j
.
org
/
codes
.
html
#
StaticLoggerBi
\
6
nder
for
further
details
.
1
<dependency>
2
<groupId>
org.slf4j</groupId>
3
<artifactId>
slf4j-simple</artifactId>
4
<version>
1.7.25</version>
5
</dependency>
Asynchronous requests
Javalin 1.6.0 introduced future results. While the default threadpool (200 threads) is enough for most use cases, sometimes slow operations should be run asynchronously. Luckily it’s very easy in Javalin, just pass a CompletableFuture
to ctx.result()
:
1
import
io.javalin.Javalin
2
3
fun
main
(
args
:
Array
<
String
>)
{
4
val
app
=
Javalin
.
start
(
7000
)
5
app
.
get
(
"/"
)
{
ctx
->
ctx
.
result
(
getFuture
())
}
6
}
7
8
// hopefully your future is less pointless than this:
9
private
fun
getFuture
()
=
CompletableFuture
<
String
>().
app
\
10
ly
{
11
Executors
.
newSingleThreadScheduledExecutor
()
12
.
schedule
({
this
.
complete
(
"Hello World!"
)
},
1
,
Time
\
13
Unit
.
SECONDS
)
14
}
You can only set future results in endpoint handlers (get/post/put/etc). After-handlers, exception-handlers and error-handlers run like you’d expect them to after the future has been resolved or rejected.
Configuring JSON Mapper and Jackson
The JSON mapper can be configured like this:
1
Gson
gson
=
new
GsonBuilder
().
create
();
2
JavalinJsonPlugin
.
setJsonToObjectMapper
(
gson
::
fromJson
);
3
JavalinJsonPlugin
.
setObjectToJsonMapper
(
gson
::
toJson
);
Configuring Jackson
The JSON mapper uses Jackson by default, which can be configured by calling:
1
JavalinJacksonPlugin
.
configure
(
objectMapper
)
Note that these are global settings, and can’t be configured per instance of Javalin.
Views and Templates
Javalin currently supports five template engines, as well as markdown:
1
ctx
.
renderThymeleaf
(
"/templateFile"
,
model
(
"firstName"
,
"\
2
John"
,
"lastName"
,
"Doe"
))
3
ctx
.
renderVelocity
(
"/templateFile"
,
model
(
"firstName"
,
"J\
4
ohn"
,
"lastName"
,
"Doe"
))
5
ctx
.
renderFreemarker
(
"/templateFile"
,
model
(
"firstName"
,
\
6
"John"
,
"lastName"
,
"Doe"
))
7
ctx
.
renderMustache
(
"/templateFile"
,
model
(
"firstName"
,
"J\
8
ohn"
,
"lastName"
,
"Doe"
))
9
ctx
.
renderJtwig
(
"/templateFile"
,
model
(
"firstName"
,
"John\
10
"
,
"lastName"
,
"Doe"
))
11
ctx
.
renderMarkdown
(
"/markdownFile"
)
12
// Javalin looks for templates/markdown files in src/reso\
13
urces
Configure:
1
JavalinThymeleafPlugin
.
configure
(
templateEngine
)
2
JavalinVelocityPlugin
.
configure
(
velocityEngine
)
3
JavalinFreemarkerPlugin
.
configure
(
configuration
)
4
JavalinMustachePlugin
.
configure
(
mustacheFactory
)
5
JavalinJtwigPlugin
.
configure
(
configuration
)
6
JavalinCommonmarkPlugin
.
configure
(
htmlRenderer
,
markdownP
\
7
arser
)
Note that these are global settings, and can’t be configured per instance of Javalin.
Chapter Three: Creating a Google Docs clone with WebSockets using Javalin
What You Will Learn
In this tutorial we will create a very simple realtime collaboration tool (like google docs). We will be using WebSockets for this, as WebSockets provides us with two-way communication over a one connection, meaning we won’t have to make additional HTTP requests to send and receive messages. A WebSocket connection stays open, greatly reducing latency (and complexity). Dependencies
Create a maven project with dependencies
We will be using Javalin for our web-server and WebSockets, and slf4j for logging:
1
<dependencies>
2
<dependency>
3
<groupId>
io.javalin</groupId>
4
<artifactId>
javalin</artifactId>
5
<version>
1.7.0</version>
6
</dependency>
7
<dependency>
8
<groupId>
org.slf4j</groupId>
9
<artifactId>
slf4j-simple</artifactId>
10
<version>
1.7.25</version>
11
</dependency>
12
</dependencies>
The Java application
The Java application is pretty straightforward. We need:
- a data class (Collab) containing the document and the collaborators
- a map to keep track of document-ids and Collabs
- websocket handlers for connect/message/close
We can get the entire server done in about 40 lines:
1
import
io.javalin.Javalin
;
2
import
io.javalin.embeddedserver.jetty.websocket.WsSessio
\
3
n
;
4
import
java.util.Map
;
5
import
java.util.concurrent.ConcurrentHashMap
;
6
7
public
class
Main
{
8
9
private
static
Map
<
String
,
Collab
>
collabs
=
new
Conc
\
10
urrentHashMap
<>();
11
12
public
static
void
main
(
String
[]
args
)
{
13
14
Javalin
.
create
()
15
.
port
(
7070
)
16
.
enableStaticFiles
(
"/public"
)
17
.
ws
(
"/docs/:doc-id"
,
ws
->
{
18
ws
.
onConnect
(
session
->
{
19
if
(
getCollab
(
session
)
==
null
)
{
20
createCollab
(
session
);
21
}
22
getCollab
(
session
).
sessions
.
add
(
sessi
\
23
on
);
24
session
.
send
(
getCollab
(
session
).
doc
);
25
});
26
ws
.
onMessage
((
session
,
message
)
->
{
27
getCollab
(
session
).
doc
=
message
;
28
getCollab
(
session
).
sessions
.
stream
().
\
29
filter
(
WsSession
::
isOpen
).
forEach
(
s
->
{
30
s
.
send
(
getCollab
(
session
).
doc
);
31
});
32
});
33
ws
.
onClose
((
session
,
status
,
message
)
->
{
34
getCollab
(
session
).
sessions
.
remove
(
se
\
35
ssion
);
36
});
37
})
38
.
start
();
39
40
}
41
42
private
static
Collab
getCollab
(
WsSession
session
)
{
43
return
collabs
.
get
(
session
.
param
(
"doc-id"
));
44
}
45
46
private
static
void
createCollab
(
WsSession
session
)
{
47
collabs
.
put
(
session
.
param
(
"doc-id"
),
new
Collab
()
\
48
);
49
}
50
51
}
We also need to create a data object for holding our document and the people working on it:
1
import
io.javalin.embeddedserver.jetty.websocket.WsSessio
\
2
n
;
3
import
java.util.Set
;
4
import
java.util.concurrent.ConcurrentHashMap
;
5
6
public
class
Collab
{
7
public
String
doc
;
8
public
Set
<
WsSession
>
sessions
;
9
10
public
Collab
()
{
11
this
.
doc
=
""
;
12
this
.
sessions
=
ConcurrentHashMap
.
newKeySet
();
13
}
14
}
Building a JavaScript Client
In order to demonstrate that our application works, we can build a JavaScript client. We’ll keep the HTML very simple, we just need a heading and a text area:
1
<
body
>
2
<
h1
>
Open the URL in another tab to start collaboratin\
3
g</
h1
>
4
<
textarea
placeholder
=
"Type something ..."
></
textarea
>
5
</
body
>
The JavaScript part could also be very simple, but we want some slightly advanced features:
- When you open the page, the app should either connect to an existing document or generate a new document with a random id
- When a WebSocket connection is closed, it should immediately be reestablished
- When new text is received, the user caret (“text-cursor”) should remain in the same location (easily the most complicated part of the tutorial).
1
window
.
onload
=
setupWebSocket
;
2
window
.
onhashchange
=
setupWebSocket
;
3
4
if
(
!
window
.
location
.
hash
)
{
// document-id not present i\
5
n
url
6
const
newDocumentId
=
Date
.
now
().
toString
(
36
);
// thi\
7
s
should
be
more
random
8
window
.
history
.
pushState
(
null
,
null
,
"#"
+
newDocumen
\
9
tId
);
10
}
11
12
function
setupWebSocket
()
{
13
const
textArea
=
document
.
querySelector
(
"textarea"
);
14
const
ws
=
new
WebSocket
(
`ws://localhost:7070/docs/
${
\
15
window
.
location
.
hash
.
substr
(
1
)
}
`
);
16
textArea
.
onkeyup
=
()
=>
ws
.
send
(
textArea
.
value
);
17
ws
.
onmessage
=
msg
=>
{
18
// place the caret in the correct position
19
const
offset
=
msg
.
data
.
length
-
textArea
.
value
.
l
\
20
ength
;
21
const
selection
=
{
start
:
textArea
.
selectionStart
\
22
,
end
:
textArea
.
selectionEnd
};
23
const
startsSame
=
msg
.
data
.
startsWith
(
textArea
.
v
\
24
alue
.
substring
(
0
,
selection
.
end
));
25
const
endsSame
=
msg
.
data
.
endsWith
(
textArea
.
value
\
26
.
substring
(
selection
.
start
));
27
textArea
.
value
=
msg
.
data
;
28
if
(
startsSame
&&
!
endsSame
)
{
29
textArea
.
setSelectionRange
(
selection
.
start
,
s
\
30
election
.
end
);
31
}
else
if
(
!
startsSame
&&
endsSame
)
{
32
textArea
.
setSelectionRange
(
selection
.
start
+
\
33
offset
,
selection
.
end
+
offset
);
34
}
else
{
// this is what google docs does...
35
textArea
.
setSelectionRange
(
selection
.
start
,
s
\
36
election
.
end
+
offset
);
37
}
38
};
39
ws
.
onclose
=
setupWebSocket
;
// should reconnect if c\
40
onnection
is
closed
41
}
And that’s it! Now try opening localhost:7070
in a couple of different browser windows (that you can see simultaneously) and collaborate with yourself.
Conclusion
We have a working realtime collaboration app written in less than 100 lines of Java and JavaScript. It’s very basic though, some things to add could include:
- Show who is currently editing the document
- Persist the data in a database at periodic intervals
- Replace the textarea with a rich text editor, such as quill
- Replace the textarea with a code editor such as ace for collaborative programming
- Improving the collaborative aspects with operational transformation
The use cases are not limited to text and documents though, you should use WebSockets for any project which requires a lot of interactions with low latency. Have fun!
Chapter Four: Creating a simple chat-app with WebSockets
Aim
In this tutorial we will create a simple real-time chat application. It will feature a chat-panel that stores messages received after you join, a list of currently connected users, and an input field to send messages from. We will be using WebSockets for this, as WebSockets provides us with full-duplex communication channels over a single TCP connection, meaning we won’t have to make additional HTTP requests to send and receive messages. A WebSocket connection stays open, greatly reducing latency (and complexity). Dependencies
First, we need to create a Maven project with some dependencies: (→ Tutorial)
1
<dependencies>
2
<dependency>
3
<groupId>
io.javalin</groupId>
4
<artifactId>
javalin</artifactId>
5
<version>
1.7.0</version>
6
</dependency>
7
<dependency>
8
<groupId>
org.slf4j</groupId>
9
<artifactId>
slf4j-simple</artifactId>
10
<version>
1.7.13</version>
11
</dependency>
12
<dependency>
13
<groupId>
org.json</groupId>
14
<artifactId>
json</artifactId>
15
<version>
20160810</version>
16
</dependency>
17
<dependency>
18
<groupId>
com.j2html</groupId>
19
<artifactId>
j2html</artifactId>
20
<version>
1.2.0</version>
21
</dependency>
22
</dependencies>
The Java application
The Java application is pretty straightforward. We need:
- a map to keep track of session/username pairs.
- a counter for number of users (nicknames are auto-incremented)
- websocket handlers for connect/message/close
- a method for broadcasting a message to all users
- a method for creating the message in HTML (or JSON if you prefer)
1
public
class
Chat
{
2
3
private
static
Map
<
Session
,
String
>
userUsernameMap
=
\
4
new
ConcurrentHashMap
<>();
5
private
static
int
nextUserNumber
=
1
;
// Assign to u\
6
sername
for
next
connecting
user
7
8
public
static
void
main
(
String
[]
args
)
{
9
Javalin
.
create
()
10
.
port
(
7070
)
11
.
enableStaticFiles
(
"/public"
)
12
.
ws
(
"/chat"
,
ws
->
{
13
ws
.
onConnect
(
session
->
{
14
String
username
=
"User"
+
nextUserNu
\
15
mber
++;
16
userUsernameMap
.
put
(
session
,
username
\
17
);
18
broadcastMessage
(
"Server"
,
(
username
\
19
+
" joined the chat"
));
20
});
21
ws
.
onClose
((
session
,
status
,
message
)
->
{
22
String
username
=
userUsernameMap
.
get
\
23
(
session
);
24
userUsernameMap
.
remove
(
session
);
25
broadcastMessage
(
"Server"
,
(
username
\
26
+
" left the chat"
));
27
});
28
ws
.
onMessage
((
session
,
message
)
->
{
29
broadcastMessage
(
userUsernameMap
.
get
(
\
30
session
),
message
);
31
});
32
})
33
.
start
();
34
}
35
36
// Sends a message from one user to all users, along \
37
with
a
list
of
current
usernames
38
private
static
void
broadcastMessage
(
String
sender
,
S
\
39
tring
message
)
{
40
userUsernameMap
.
keySet
().
stream
().
filter
(
Session
:
\
41
:
isOpen
).
forEach
(
session
->
{
42
try
{
43
session
.
getRemote
().
sendString
(
44
new
JSONObject
()
45
.
put
(
"userMessage"
,
createHtmlMes
\
46
sageFromSender
(
sender
,
message
))
47
.
put
(
"userlist"
,
userUsernameMap
.
\
48
values
()).
toString
()
49
);
50
}
catch
(
Exception
e
)
{
51
e
.
printStackTrace
();
52
}
53
});
54
}
55
56
// Builds a HTML element with a sender-name, a messag\
57
e
,
and
a
timestamp
58
private
static
String
createHtmlMessageFromSender
(
Str
\
59
ing
sender
,
String
message
)
{
60
return
article
(
61
b
(
sender
+
" says:"
),
62
span
(
attrs
(
".timestamp"
),
new
SimpleDateForma
\
63
t
(
"HH:mm:ss"
).
format
(
new
Date
())),
64
p
(
message
)
65
).
render
();
66
}
67
68
}
Building a JavaScript Client
In order to demonstrate that our application works, we can build a JavaScript client. First we create our index.html:
1
<!DOCTYPE html>
2
<
html
>
3
<
head
>
4
<
meta
name
=
"viewport"
content
=
"width=device-width, in\
5
itial-scale=1"
>
6
<
title
>
WebsSockets</
title
>
7
<
link
rel
=
"stylesheet"
href
=
"style.css"
>
8
</
head
>
9
<
body
>
10
<
div
id
=
"chatControls"
>
11
<
input
id
=
"message"
placeholder
=
"Type your messag\
12
e"
>
13
<
button
id
=
"send"
>
Send</
button
>
14
</
div
>
15
<
ul
id
=
"userlist"
>
<!-- Built by JS -->
</
ul
>
16
<
div
id
=
"chat"
>
<!-- Built by JS -->
</
div
>
17
<
script
src
=
"websocketDemo.js"
></
script
>
18
</
body
>
19
</
html
>
As you can see, we reference a stylesheet called style.css, which can be found on GitHub.
The final step needed for completing our chat application is creating websocketDemo.js:
1
// small helper function for selecting element by id
2
let
id
=
id
=>
document
.
getElementById
(
id
);
3
4
//Establish the WebSocket connection and set up event han\
5
dlers
6
let
ws
=
new
WebSocket
(
"ws://"
+
location
.
hostname
+
":"
\
7
+
location
.
port
+
"/chat"
);
8
ws
.
onmessage
=
msg
=>
updateChat
(
msg
);
9
ws
.
onclose
=
()
=>
alert
(
"WebSocket connection closed"
);
10
11
// Add event listeners to button and input field
12
id
(
"send"
).
addEventListener
(
"click"
,
()
=>
sendAndClear
(
i
\
13
d
(
"message"
).
value
));
14
id
(
"message"
).
addEventListener
(
"keypress"
,
function
(
e
)
{
15
if
(
e
.
keyCode
===
13
)
{
// Send message if enter is p\
16
ressed
in
input
field
17
sendAndClear
(
e
.
target
.
value
);
18
}
19
});
20
21
function
sendAndClear
(
message
)
{
22
if
(
message
!==
""
)
{
23
ws
.
send
(
message
);
24
id
(
"message"
).
value
=
""
;
25
}
26
}
27
28
function
updateChat
(
msg
)
{
// Update chat-panel and list \
29
of
connected
users
30
let
data
=
JSON
.
parse
(
msg
.
data
);
31
id
(
"chat"
).
insertAdjacentHTML
(
"afterbegin"
,
data
.
user
\
32
Message
);
33
id
(
"userlist"
).
innerHTML
=
data
.
userlist
.
map
(
user
=>
\
34
"<li>"
+
user
+
"</li>"
).
join
(
""
);
35
}
And that’s it! Now try opening localhost:7070 in a couple of different browser windows (that you can see simultaneously) and talk to yourself.
Conclusion
Well, that was easy! We have a working real-time chat application implemented without polling, written in a total of less than 100 lines of Java and JavaScript. The implementation is very basic though, and we should at least split up the sending of the userlist and the messages (so that we don’t rebuild the user list every time anyone sends a message), but since the focus of this tutorial was supposed to be on WebSockets, I chose to do the implementation as minimal as I could be comfortable with.
Chapter Five: Working with HTML forms and a Java backend
Dependencies
First, we need to create a project with these dependencies: (→ Tutorial)
1
<dependencies>
2
<dependency>
3
<groupId>
io.javalin</groupId>
4
<artifactId>
javalin</artifactId>
5
<version>
1.7.0</version>
6
</dependency>
7
<dependency>
8
<groupId>
org.slf4j</groupId>
9
<artifactId>
slf4j-simple</artifactId>
10
<version>
1.7.13</version>
11
</dependency>
12
</dependencies>
Setting up the backend
Create a Java file, for example Main.java, that has the following code:
1
import
java.util.HashMap
;
2
import
java.util.Map
;
3
4
import
io.javalin.Javalin
;
5
6
public
class
Main
{
7
8
static
Map
<
String
,
String
>
reservations
=
new
HashMap
\
9
<
String
,
String
>()
{{
10
put
(
"saturday"
,
"No reservation"
);
11
put
(
"sunday"
,
"No reservation"
);
12
}};
13
14
public
static
void
main
(
String
[]
args
)
{
15
16
Javalin
app
=
Javalin
.
create
()
17
.
port
(
7777
)
18
.
enableStaticFiles
(
"/public"
)
19
.
start
();
20
21
app
.
post
(
"/make-reservation"
,
ctx
->
{
22
reservations
.
put
(
ctx
.
formParam
(
"day"
),
ctx
.
fo
\
23
rmParam
(
"time"
));
24
ctx
.
html
(
"Your reservation has been saved"
);
25
});
26
27
app
.
get
(
"/check-reservation"
,
ctx
->
{
28
ctx
.
html
(
reservations
.
get
(
ctx
.
queryParam
(
"day\
29
"
)));
30
});
31
32
}
33
34
}
This will create an app which listens on port 7777, and looks for static files in your /src/resources/public folder. We have two endpoints mapped, one post, which will make a reservation, and one get, which will check your reservation. Settings up the HTML forms
Now we have to make two HTML forms for interacting with these endpoints. We can put these forms in a file /resources/public/index.html, which will be available at http://localhost:7777/. Make reservation form
1
<
h2
>
Make reservation:</
h2
>
2
<
form
method
=
"post"
action
=
"/make-reservation"
>
3
Choose day
4
<
select
name
=
"day"
>
5
<
option
value
=
"saturday"
>
Saturday</
option
>
6
<
option
value
=
"sunday"
>
Sunday</
option
>
7
</
select
>
8
<
br
>
9
Choose time
10
<
select
name
=
"time"
>
11
<
option
value
=
"8:00 PM"
>
8:00 PM</
option
>
12
<
option
value
=
"9:00 PM"
>
9:00 PM</
option
>
13
</
select
>
14
<
br
>
15
<
button
>
Submit</
button
>
16
</
form
>
To make a reservation we need to create something on the server (in this case it’s a simple map.put(), but usually you’d have a database). When creating something on the server, you should use the POST method, which can be specified by adding method=”post” to the <form> element.
In our Java code, we have a post endpoint: app.post(“/make-reservation”, ctx -> {…}. We need to tell our form to use this endpoint with the action attribute: action=”/make-reservation”. Actions are relative, so when you click submit, the browser will create a POST request to http://localhost:7777/make-reservation with the day/time values as the request-body. Check reservation form
1
<
h2
>
Check your reservation:</
h2
>
2
<
form
method
=
"get"
action
=
"/check-reservation"
>
3
Choose day
4
<
select
name
=
"day"
>
5
<
option
value
=
"saturday"
>
Saturday</
option
>
6
<
option
value
=
"sunday"
>
Sunday</
option
>
7
</
select
>
8
<
br
>
9
<
button
>
Submit</
button
>
10
</
form
>
To check a reservation we need to tell the server which day we’re interested in. In this case we’re not creating anything, and our action does not change the state of the server in any way, which makes it a good candidate for a GET request.
GET requests don’t have a request-body so when you click submit the browser creates a GET request to http://localhost:7777/check-reservation?day=saturday. The values of the form are added to the URL as query-parameters. HTML form GET vs POST summary
- POST requests should be used if the request can change the server state.
- POST requests have their information stored in the request-body. In order to extract information from this body you have to use ctx.formParam(key) in Javalin.
- Performing a series of GET requests should always return the same result (if no other POST request was performed in-between).
- GET requests have no request-body, and form information is sent as query-parameters in the URL. In order to extract information from this body you have to use ctx.queryParam(key) in Javalin.
File upload example
Let’s expand our example a bit to include file uploads. We need to add a new dependency, a new endpoint, and a new form. Dependency
We need to add a dependency for handling file-uploads:
1
<dependency>
2
<groupId>
commons-fileupload</groupId>
3
<artifactId>
commons-fileupload</artifactId>
4
<version>
1.3.3</version>
5
</dependency>
Endpoint
1
app
.
post
(
"/upload-example"
,
ctx
->
{
2
ctx
.
uploadedFiles
(
"files"
).
forEach
(
file
->
{
3
try
{
4
FileUtils
.
copyInputStreamToFile
(
file
.
getConte
\
5
nt
(),
new
File
(
"upload/"
+
file
.
getName
()));
6
ctx
.
html
(
"Upload successful"
);
7
}
catch
(
IOException
e
)
{
8
ctx
.
html
(
"Upload failed"
);
9
}
10
});
11
});
ctx.uploadedFiles(“files”) gives us a list of files matching the name files. We then save these files to an upload folder.
HTML form
1
<
h1
>
Upload example</
h1
>
2
<
form
method
=
"post"
action
=
"/upload-example"
enctype
=
"mul\
3
tipart/form-data"
>
4
<
input
type
=
"file"
name
=
"files"
multiple
>
5
<
button
>
Submit</
button
>
6
</
form
>
When uploading files you need to add enctype=”multipart/form-data” to your <form>. If you want to upload multiple files, add the multiple attribute to your <input>.
Chapter Six: A comparison with Spark
<h1 class=”no-margin-top”>SparkJava and Javalin comparison</h1> People often ask about the differences between Spark and Javalin. This page shows some of them. It’s not intended to tell you which library is better, just to highlight the differences.
Handlers vs Routes and Filters
Javalin has the concept of a Handler
. A Handler
is void and takes a Context
(which wraps HttpServletRequest
and HttpServletResponse
),
and you operate on both the request and response through this Context
.
1
app
.
get
(
"/path"
,
ctx
->
ctx
.
result
(
"Hello, World!"
));
2
app
.
after
(
"/path"
,
ctx
->
ctx
.
result
(
"Actually, nevermind\
3
..."
));
Spark on the other hand has Route
s and Filter
s. Both Route
and Filter
in Spark take
(Request
, Response
) as input. Route
has a return value (Object
), while Filter
is void.
Request
and Response
wrap HttpServletRequest
and HttpServletResponse
, respectively.
1
app
.
get
(
"/path"
,
(
req
,
res
)
->
"Hello, World!"
);
2
app
.
after
(
"/path"
,
(
req
,
res
)
->
res
.
body
(
"Actually, neve\
3
rmind..."
));
Some other differences:
- Spark can run on an application server. Javalin can only run on the included embedded Jetty server.
- Javalin supports a lot of configuration options (HTTP2, Jetty Handler chains, fully customizable Jetty
Server
, CORS, default values). Spark has sane defaults, but limited configuration options. - Spark has a static API and an instance API. Javalin only has an instance API.
- Javalin focuses on Java/Kotlin interoperability and behaves the same in both languages. Spark has Kotlin DSL (spark-kotlin) built on top of itself with “Kotlin only” features.
- Spark supports route mapping based on accepts/content type. Javalin doesn’t.
- Javalin supports regex routes. Spark doesn’t.
- Javalin has a lambda based WebSocket API which supports path-params (and other things). Spark uses an annotation based API which proxies directly to Jetty.
- Spark has a redirect DSL
redirect.get("/fromPath", "/toPath");
. Javalin doesn’t. - Javalin has async support for long running tasks via
CompletableFuture
. Spark doesn’t. - Javalin supports context extensions (
ctx.extension(MyExt.class).myMethod()
). Spark doesn’t. - Spark has a pac4j library. Javalin doesn’t.
- Javalin has an
AccessManager
interfaces with role-support. Spark doesn’t. - Spark is written in Java. Javalin is written in Kotlin.
- Javalin has lifecycle events. Spark doesn’t.
Chapter 7: Javalin and Docker
What is Docker?
1
Docker is a tool designed to make it easier to create\
2
, deploy, and run applications by using containers.
3
Containers allow a developer to package up an applica\
4
tion with all of the parts it needs,
5
such as libraries and other dependencies, and ship it\
6
all out as one package.
7
By doing so, thanks to the container, the developer c\
8
an rest assured that the application will run on any othe\
9
r
10
Linux machine regardless of any customized settings t\
11
hat machine might have that could differ from the machine
12
used for writing and testing the code.
13
In a way, Docker is a bit like a virtual machine. But\
14
unlike a virtual machine, rather than creating a whole
15
virtual operating system, Docker allows applications \
16
to use the same Linux kernel as the system that they're
17
running on and only requires applications be shipped \
18
with things not already running on the host computer.
19
This gives a significant performance boost and reduce\
20
s the size of the application.
21
— <a href="https://opensource.com/resources/wha\
22
t-docker">opensource.com</a>
Initial Setup
Before we get started, there are a few things we need to do:
- Set up Docker (Install Docker)
- Deploy Registry server (Deploy Registry)
- Install Maven
- Set up the Javalin Hello World example with Maven (→ Tutorial)
Configuring Maven
This is actually where most of the work is done. In order to easily deploy a Java application anywhere, you have to create a jar file containing your application and all of its dependencies. Open the pom.xml of your Javalin Maven project and add the following configuration (below your dependencies tag):
1
<distributionManagement>
2
<repository>
3
<id>
repo</id>
4
<name>
Internal release</name>
5
<url>
your-snapshot-repo-url</url>
6
</repository>
7
</distributionManagement>
8
9
<build>
10
<plugins>
11
<plugin>
12
<groupId>
org.apache.maven.plugins</groupId>
13
<artifactId>
maven-compiler-plugin</artifactId>
14
<version>
2.3.2</version>
15
<configuration>
16
<source>
1.8</source>
17
<target>
1.8</target>
18
</configuration>
19
</plugin>
20
<plugin>
21
<groupId>
org.apache.maven.plugins</groupId>
22
<artifactId>
maven-shade-plugin</artifactId>
23
<version>
3.1.0</version>
24
<executions>
25
<!-- Run shade goal on package phase -->
26
<execution>
27
<phase>
package</phase>
28
<goals>
29
<goal>
shade</goal>
30
</goals>
31
<configuration>
32
<shadedArtifactAttached>
true<
/sha\
33
dedArtifactAttached>
34
<transformers>
35
<!-- add Main-Class to manife\
36
st file -->
37
<transformer
38
implementation=
"org.a\
39
pache.maven.plugins.shade.resource.ManifestResourceTransf\
40
ormer"
>
41
<mainClass>
HelloWorld<
/ma\
42
inClass>
43
</transformer>
44
</transformers>
45
</configuration>
46
</execution>
47
</executions>
48
</plugin>
49
</plugins>
50
</build>
Configuring Docker
Before we can configure anything we must create a Dockerfile. We can create a text file using any editor and name it Dockerfile. Copy below contents to the Dockerfile and move this file to root of your project.
1
FROM openjdk:8-jre-alpine
2
3
EXPOSE 7000
4
ENTRYPOINT ["/usr/bin/java", "-jar", "/usr/share/javalin/\
5
my-javalin.jar"]
6
7
ARG JAR_FILE
8
ADD target/${
JAR_FILE
}
/usr/share/javalin/my-javalin.jar
When you’ve added the Dockerfile to your project, it should look like this
To build a docker image for your application we will use dockerfile-maven-plugin (dockerfile-maven-plugin). Set DOCKER_HOST environment variable as mentioned (here). In the repository section “localhost:5000” is the registry url.
1
<plugin>
2
<groupId>
com.spotify</groupId>
3
<artifactId>
dockerfile-maven-plugin</artifactId>
4
<version>
1.3.6</version>
5
<executions>
6
<execution>
7
<id>
default</id>
8
<goals>
9
<goal>
build</goal>
10
<goal>
push</goal>
11
</goals>
12
</execution>
13
</executions>
14
<configuration>
15
<repository>
localhost:5000/javalin</repository>
16
<tag>
${project.version}</tag>
17
<buildArgs>
18
<JAR_FILE>
${project.build.finalName}-shaded.j\
19
ar</JAR_FILE>
20
</buildArgs>
21
</configuration>
22
</plugin>
When you’ve added the Docker config to your pom, it should look like this
Making Javalin Listen on the Correct Port
The only thing left is making sure Javalin can handle your requests. We have exposed port 7000 in Dockerfile. That means that 7000 port on the container is accessible to the outside world. So we will configure Javalin to listen on “7000”
1
import
io.javalin.Javalin
;
2
3
public
class
HelloWorld
{
4
5
public
static
void
main
(
String
[]
args
)
{
6
Javalin
app
=
Javalin
.
start
(
7000
);
7
app
.
get
(
"/"
,
ctx
->
ctx
.
result
(
"Hello World"
));
8
}
9
10
}
## Build and Push Docker image
Now we can deploy our application using mvn deploy
.
This will build the docker image and push it to your registry server.
Image name is same as repository value in the pom.
Additionally we add a tag to image to specify images for different versions.
So image name for this example is localhost:5000/javalin:1.0.0-SNAPSHOT.
Again, make sure you’re in your project root, then enter:
1
mvn deploy
Run Docker image
Now we can run our application using docker run
.
open terminal then enter:
1
docker run -d -p 7000:7000 localhost:5000/javalin:1.0.0-S\
2
NAPSHOT
That’s it. Our application is now available at http://localhost:7000/
Chapter 8: Sending email with Javalin
Dependencies
First, we need to create a Maven project with some dependencies: (→ Tutorial)
1
<dependencies>
2
<dependency>
3
<groupId>
io.javalin</groupId>
4
<artifactId>
javalin</artifactId>
<!-- For handlin\
5
g http-requests -->
6
<version>
{{site.javalinversion}}</version>
7
</dependency>
8
<dependency>
9
<groupId>
org.apache.commons</groupId>
10
<artifactId>
commons-email</artifactId>
<!-- For s\
11
ending emails -->
12
<version>
1.4</version>
13
</dependency>
14
<dependency>
15
<groupId>
com.j2html</groupId>
16
<artifactId>
j2html</artifactId>
<!-- For creating\
17
HTML form -->
18
<version>
1.0.0</version>
19
</dependency>
20
<dependency>
21
<groupId>
org.slf4j</groupId>
22
<artifactId>
slf4j-simple</artifactId>
<!-- For lo\
23
gging -->
24
<version>
1.7.25</version>
25
</dependency>
26
</dependencies>
Setting up the backend
We need three endpoints: GET '/'
, POST '/contact-us'
and GET '/contact-us/success'
:
1
import
org.apache.commons.mail.*
;
2
import
io.javalin.Javalin
;
3
import static
j2html.TagCreator.*
;
4
5
public
class
Main
{
6
7
public
static
void
main
(
String
[]
args
)
{
8
9
Javalin
app
=
Javalin
.
create
()
10
.
port
(
7000
)
11
.
start
();
12
13
app
.
get
(
"/"
,
ctx
->
ctx
.
html
(
14
form
().
withAction
(
"/contact-us"
).
withMethod
(
"\
15
post"
).
with
(
16
input
().
withName
(
"subject"
).
withPlacehold
\
17
er
(
"Subject"
),
18
br
(),
19
textarea
().
withName
(
"message"
).
withPlaceh
\
20
older
(
"Your message ..."
),
21
br
(),
22
button
(
"Submit"
)
23
).
render
()
24
));
25
26
app
.
post
(
"/contact-us"
,
ctx
->
{
27
Email
email
=
new
SimpleEmail
();
28
email
.
setHostName
(
"smtp.googlemail.com"
);
29
email
.
setSmtpPort
(
465
);
30
email
.
setAuthenticator
(
new
DefaultAuthenticat
\
31
or
(
"YOUR_EMAIL"
,
"YOUR_PASSWORD"
));
32
email
.
setSSLOnConnect
(
true
);
33
email
.
setFrom
(
"YOUR_EMAIL"
);
34
email
.
setSubject
(
ctx
.
formParam
(
"subject"
));
/
\
35
/
subject
from
HTML
-
form
36
email
.
setMsg
(
ctx
.
formParam
(
"message"
));
// me\
37
ssage
from
HTML
-
form
38
email
.
addTo
(
"RECEIVING_ADDRESS"
);
39
email
.
send
();
// will throw email-exception i\
40
f
something
is
wrong
41
ctx
.
redirect
(
"/contact-us/success"
);
42
});
43
44
app
.
get
(
"/contact-us/success"
,
ctx
->
ctx
.
html
(
"Y\
45
our message was sent"
));
46
47
}
48
49
}
50
~~~~~~
51
52
In
order
to
get
the
above
code
to
work
,
you
need
to
make
\
53
some
changes
:
54
55
*
Change
`
YOUR_EMAIL
`
to
your
gmail
account
<
small
>(
youre
\
56
mail
@gmail.com
)</
small
>
57
*
Change
`
YOUR_PASSWORD
`
to
your
gmail
password
*
58
*
Change
`
RECEIVING_ADDRESS
`
to
where
you
want
the
email
\
59
to
be
sent
60
61
**
It
might
be
a
good
idea
to
create
a
test
-
account
instea
\
62
d
of
using
your
real
gmail
credentials
.*
63
64
When
you
have
made
the
changes
to
the
code
,
run
the
progr
\
65
am
and
go
to
`
http
:
//localhost:7000`.
66
You
will
see
a
simple
unstyled
form
with
an
input
field
,
\
67
a
textarea
and
a
button
.
68
Fill
in
the
form
and
click
the
button
to
test
your
email
\
69
server
.
After
you
click
the
button
,
your
browser
70
is
redirected
to
`
/
contact
-
us
/
success
`
(
if
the
email
was
\
71
sent
).
72
73
Any
emails
you
have
sent
will
show
up
in
your
`
Sent
`
fold
\
74
er
in
the
gmail
web
-
interface
.
75
76
{
bump
-
link
-
number
}
77
78
{
leanpub
-
filename
=
"chapter9.txt"
}
79
80
#
Chapter
9
:
Deploying
Javalin
apps
to
Heroku
81
82
##
What
is
Heroku
?
83
Heroku
is
a
cloud
application
platform
–
a
new
wa
\
84
y
of
building
and
deploying
web
apps
.
85
Our
service
lets
app
developers
spend
their
time
\
86
on
their
application
code
,
87
not
managing
servers
,
deployment
,
ongoing
operati
\
88
ons
,
or
scaling
.
89
&
mdash
;
<
a
href
=
"https://www.heroku.com/about"
>
he
\
90
roku
.
com
</
a
>
91
92
Heroku
takes
care
of
everything
related
to
deployment
,
an
\
93
d
gives
94
you
easy
access
to
key
commands
via
their
tool
Heroku
Too
\
95
lbelt
.
96
It
'
s
very
easy
to
get
started
with
(
as
you
'
ll
soon
learn
)
\
97
,
and
it
98
provides
a
nice
free
-
tier
that
you
can
use
to
deploy
your
\
99
webapps
.
100
101
##
Initial
Setup
102
Before
we
get
started
,
there
are
a
few
things
we
need
to
\
103
do
:
104
105
*
Create
a
free
Heroku
account
[(
sign
up
)](
https
:
//signup\
106
.
heroku
.
com
/
dc
)
107
*
Install
[
Heroku
Toolbelt
](
https
:
//toolbelt.heroku.com/)
108
*
Install
[
Maven
](
https
:
//maven.apache.org/guides/getting\
109
-
started
/
maven
-
in
-
five
-
minutes
.
html
)
110
*
Set
up
the
Javalin
Hello
World
example
with
Maven
[(
→
T
\
111
utorial
)](/
tutorials
/
maven
-
setup
)
112
113
##
Configuring
Maven
114
This
is
actually
where
most
of
the
work
is
done
.
In
order
\
115
to
easily
116
deploy
a
Java
application
anywhere
,
you
have
to
create
a
\
117
jar
file
118
containing
your
application
and
all
of
its
dependencies
.
119
Open
the
pom
.
xml
of
your
Javalin
Maven
project
and
add
the
120
following
configuration
(
below
your
dependencies
tag
):
121
122
{
title
=
"pom.xml"
,
lang
=
xml
}
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>Main</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build>
Configuring Heroku
Before we can configure anything, we actually have to create a
Heroku application. This can be done by using the heroku create
command.
Open a terminal and navigate to your project root, then enter:
1
heroku create javalin-heroku-example #choose your own app\
2
lication name
Now that you have a Heroku application, we have to configure how to deploy it using Maven. This is pretty straightforward using the Heroku Maven plugin.
We specify the JDK version and the app-name, along with the launch config:
1
<plugin>
2
<groupId>
com.heroku.sdk</groupId>
3
<artifactId>
heroku-maven-plugin</artifactId>
4
<version>
1.1.3</version>
5
<configuration>
6
<jdkVersion>
1.8</jdkVersion>
7
<appName>
javalin-heroku-example</appName>
8
<processTypes>
9
<!-- Tell Heroku how to launch your applicati\
10
on -->
11
<web>
java -jar ./target/javalin-heroku-exampl\
12
e-1.0-jar-with-dependencies.jar</web>
13
</processTypes>
14
</configuration>
15
</plugin>
When you’ve added the Heroku config to your pom, it should look like this
Making Javalin Listen on the Correct Port
The only thing left is making sure Javalin can handle your requests. Heroku assigns your application a new port every time you deploy it, so we have to get this port and tell Javalin to use it:
1
import
io.javalin.Javalin
;
2
3
public
class
Main
{
4
5
public
static
void
main
(
String
[]
args
)
{
6
Javalin
app
=
Javalin
.
create
()
7
.
port
(
getHerokuAssignedPort
())
8
.
start
()
9
.
get
(
"/"
,
ctx
->
ctx
.
result
(
"Hello Heroku"
));
10
}
11
12
private
static
int
getHerokuAssignedPort
()
{
13
ProcessBuilder
processBuilder
=
new
ProcessBuilde
\
14
r
();
15
if
(
processBuilder
.
environment
().
get
(
"PORT"
)
!=
n
\
16
ull
)
{
17
return
Integer
.
parseInt
(
processBuilder
.
enviro
\
18
nment
().
get
(
"PORT"
));
19
}
20
return
7000
;
21
}
22
23
}
Now we can deploy our application using mvn heroku:deploy
.
Again, make sure you’re in your project root, then enter:
1
mvn heroku:deploy
That’s it. Our application is now available at https://<appname>.herokuapp.com/
Chapter 10: Authentication with Javalin
Dependencies
First, create a new Gradle project with the following dependencies: (→ Tutorial)
1
dependencies {
2
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kot\
3
lin_version"
4
compile "io.javalin:javalin:
{{
site.javalinversion
}}
"
5
compile "com.fasterxml.jackson.module:jackson-module-\
6
kotlin:2.8.4"
7
compile "org.slf4j:slf4j-simple:1.7.22"
8
}
## Creating controllers We need something worth protecting. Let’s pretend we have a very important API for manipulating a user database. We make a controller-object with some dummy data and CRUD operations:
1
import
io.javalin.Context
2
import
java.util.*
3
4
object
UserController
{
5
6
private
data
class
User
(
val
name
:
String
,
val
email
:
\
7
String
)
8
9
private
val
users
=
hashMapOf
(
10
randomId
()
to
User
(
name
=
"Alice"
,
email
=
"a\
11
lice@alice
.
kt
"),
12
randomId
()
to
User
(
name
=
"Bob"
,
email
=
"bob\
13
@bob
.
kt
"),
14
randomId
()
to
User
(
name
=
"Carol"
,
email
=
"c\
15
arol@carol
.
kt
"),
16
randomId
()
to
User
(
name
=
"Dave"
,
email
=
"da\
17
ve@dave
.
kt
")
18
)
19
20
fun
getAllUserIds
(
ctx
:
Context
)
{
21
ctx
.
json
(
users
.
keys
)
22
}
23
24
fun
createUser
(
ctx
:
Context
)
{
25
users
[
randomId
()]
=
ctx
.
bodyAsClass
(
User
::
class
.
j
\
26
ava
)
27
}
28
29
fun
getUser
(
ctx
:
Context
)
{
30
ctx
.
json
(
users
[
ctx
.
param
(
":user-id"
)
!!
]
!!
)
31
}
32
33
fun
updateUser
(
ctx
:
Context
)
{
34
users
[
ctx
.
param
(
":user-id"
)
!!
]
=
ctx
.
bodyAsClass
(
\
35
User
::
class
.
java
)
36
}
37
38
fun
deleteUser
(
ctx
:
Context
)
{
39
users
.
remove
(
ctx
.
param
(
":user-id"
)
!!
)
40
}
41
42
private
fun
randomId
()
=
UUID
.
randomUUID
().
toString
()
43
44
}
We’re using !!
to convert nullables to non-nullables.
If :user-id
is missing or users[id]
returns null, we’ll get a NullPointerException
and our application will crash. Handling this is outside the scope of the tutorial.
Creating roles
Now that we have our functionality, we need to define a set of roles for our system.
This is done by implementing the Role
interface from io.javalin.security.Role
.
We’ll define three roles, one for “anyone”, one for permission to read user-data,
and one for permission to write user-data.
1
enum
class
ApiRole
: Role
{ ANYONE
, USER_READ
, USER_WRITE
\
2
}
Setting up the API
Now that we have roles, we can implement our endpoints:
1
import
io.javalin.ApiBuilder.*
2
import
io.javalin.Javalin
3
import
io.javalin.security.Role.roles
4
5
fun
main
(
vararg
args
:
String
)
{
6
7
val
app
=
Javalin
.
create
().
apply
{
8
accessManager
(
Auth
::
accessManager
)
9
}.
start
()
10
11
app
.
routes
{
12
path
(
"users"
)
{
13
get
(
UserController
::
getAllUserIds
,
roles
(
ApiR
\
14
ole
.
ANYONE
))
15
post
(
UserController
::
createUser
,
roles
(
ApiRol
\
16
e
.
USER_WRITE
))
17
path
(
":user-id"
)
{
18
get
(
UserController
::
getUser
,
roles
(
ApiRol
\
19
e
.
USER_READ
))
20
patch
(
UserController
::
updateUser
,
roles
(
A
\
21
piRole
.
USER_WRITE
))
22
delete
(
UserController
::
deleteUser
,
roles
(
\
23
ApiRole
.
USER_WRITE
))
24
}
25
}
26
}
27
28
}
A role has now been given to every endpoint:
* ANYONE
can getAllUserIds
* USER_READ
can getUser
* USER_WRITE
can createUser
, updateUser
and deleteUser
Now, all that remains is to implement the access-manager (Auth::accessManager
).
Implementing auth
The AccessManager
interface in Javalin is pretty simple.
It takes a Handler
a Context
and a list of Role
s.
The idea is that you implement code to run the handler
based on what’s in the context, and what roles are set for the endpoint.
The rules for our access manager are also simple:
* When endpoint has ApiRole.ANYONE
, all requests will be handled
* When endpoint has another role set and the request has matching credentials, the request will be handled
* Else we ignore the request and send 401 Unauthorized
back to the client
This translates nicely into Kotlin:
1
fun
accessManager
(
handler
:
Handler
,
ctx
:
Context
,
permitt
\
2
edRoles
:
List
<
Role
>)
{
3
when
{
4
permittedRoles
.
contains
(
ApiRole
.
ANYONE
)
->
handle
\
5
r
.
handle
(
ctx
)
6
ctx
.
userRoles
.
any
{
it
in
permittedRoles
}
->
han
\
7
dler
.
handle
(
ctx
)
8
else
->
ctx
.
status
(
401
).
json
(
"Unauthorized"
)
9
}
10
}
Extracting user-roles from the context
There is no ctx.userRoles
concept in Javalin, so we need to implement it.
First we need a user-table. We’ll create a map(Pair<String, String>, List<Role>)
where keys are
username+password in cleartext (please don’t do this for a real service), and values are user-roles:
1
private
val
userRoleMap
=
hashMapOf
(
2
Pair
(
"alice"
,
"weak-password"
)
to
listOf
(
ApiRole
.
\
3
USER_READ
),
4
Pair
(
"bob"
,
"better-password"
)
to
listOf
(
ApiRole
.
\
5
USER_READ
,
ApiRole
.
USER_WRITE
)
6
)
Now that we have a user-table, we need to authenticate the requests.
We do this by getting the username+password from the Basic-auth-header
and using them as keys for the userRoleMap
:
1
private
val
Context
.
userRoles
:
List
<
ApiRole
>
2
get
()
=
this
.
basicAuthCredentials
()
?.
let
{
(
username
,
\
3
password
)
->
4
userRoleMap
[
Pair
(
username
,
password
)]
?:
listOf
()
5
}
?:
listOf
()
When using basic auth, credentials are transferred as plain text (although base64-encoded). Remember to enable SSL if you’re using basic-auth for a real service.**
Conclusion
This tutorial showed one possible way of implementing an AccessManager
in Javalin, but
the interface is very flexible and you can really do whatever you want:
1
app
.
accessManager
(
handler
,
ctx
,
permittedRoles
)
->
{
2
when
{
3
ctx
.
host
().
contains
(
"localhost"
)
->
handler
.
handl
\
4
e
(
ctx
)
5
Math
.
random
()
>
0.5
->
handler
.
handle
(
ctx
)
6
dayOfWeek
==
Calendar
.
SUNDAY
->
handler
.
handle
(
ct
\
7
x
)
8
else
->
ctx
.
status
(
401
).
json
(
"Unauthorized"
)
9
}
10
};