What You Will Learn
You will learn how to create a basic Spark application with filters, controllers, views, authentication, localization, error handling, and more, but this is not really a full blown tutorial, it’s more a description of a basic structure, with certain points of the code highlighted. To get the full benefit of this tutorial, please clone the example on GitHub, run it, and play around.
Screenshot
The application is not very fancy, but offers an entity-overview, a locale-switcher, and login/logout functionality, which are all pretty essential pieces of any larger webapp.
Package structure
As you can see, the app is packaged by feature and not by layer. If you need to be convinced that this is a good approach, please have a look at this talk by Robert C. Martin.
Application.java
This is the class that ties your app together. When you open this class, you should get an immediate understanding of how everything works:
public class Application {
// Declare dependencies
public static BookDao bookDao;
public static UserDao userDao;
public static void main(String[] args) {
// Instantiate your dependencies
bookDao = new BookDao();
userDao = new UserDao();
// Configure Spark
port(4567);
staticFiles.location("/public");
staticFiles.expireTime(600L);
enableDebugScreen();
// Set up before-filters (called before each get/post)
before("*", Filters.addTrailingSlashes);
before("*", Filters.handleLocaleChange);
// Set up routes
get(Path.Web.INDEX, IndexController.serveIndexPage);
get(Path.Web.BOOKS, BookController.fetchAllBooks);
get(Path.Web.ONE_BOOK, BookController.fetchOneBook);
get(Path.Web.LOGIN, LoginController.serveLoginPage);
post(Path.Web.LOGIN, LoginController.handleLoginPost);
post(Path.Web.LOGOUT, LoginController.handleLogoutPost);
get("*", ViewUtil.notFound);
//Set up after-filters (called after each get/post)
after("*", Filters.addGzipHeader);
}
}
Static dependencies?
This is probably not what you learned in Java class, but I believe statics are better than dependency injection when dealing with web applications / controllers. Injecting dependencies makes everything a lot more ceremonious, and as can be seen in this example you need about twice the amount of code for the same functionality. I think it complicates things wihtout providing any real benefit, and before you say unit-testing: you’re not launching this thing into space, so you don’t need to test everything. If you want to test your controllers, then acceptance-tests are superior to mocking and unit-tests, as they test your application in the extact state it’ll be in when it’s deployed.
Before, routes, after
If your application is small, delcaring before-filters, routes, and after-filters all in the same location greatly improves the readability of your code. Just by looking at the class above, you can tell that there’s a filter that adds trailing slashes to all endpoints (ex: /books -> /books/) and that any page can handle a locale change. You also get an overview of all the endpoints, and see that all routes are GZIPed after everything else.
Path.Web and Controller.field
It’s usually a good idea to keep your paths in some sort of constant. In the above class I have a Path class with a static nested class Web (it also has a static nested class Template), which holds public final static Strings. That’s just my preference, it’s up to you how you want to do this. All my handlers are declared as static Route fields, grouping together functionality in the same classes (based on feature). Let’s have a look at the LoginController:
public class LoginController {
public static Route serveLoginPage = (Request request, Response response) -> {
Map<String, Object> model = new HashMap<>();
model.put("loggedOut", removeSessionAttrLoggedOut(request));
model.put("loginRedirect", removeSessionAttrLoginRedirect(request));
return ViewUtil.render(request, model, Path.Template.LOGIN);
};
public static Route handleLoginPost = (Request request, Response response) -> {
Map<String, Object> model = new HashMap<>();
if (!UserController.authenticate(getQueryUsername(request), getQueryPassword(request))) {
model.put("authenticationFailed", true);
return ViewUtil.render(request, model, Path.Template.LOGIN);
}
model.put("authenticationSucceeded", true);
request.session().attribute("currentUser", getQueryUsername(request));
if (getQueryLoginRedirect(request) != null) {
response.redirect(getQueryLoginRedirect(request));
}
return ViewUtil.render(request, model, Path.Template.LOGIN);
};
public static Route handleLogoutPost = (Request request, Response response) -> {
request.session().removeAttribute("currentUser");
request.session().attribute("loggedOut", true);
response.redirect(Path.Web.LOGIN);
return null;
};
// The origin of the request (request.pathInfo()) is saved in the session so
// the user can be redirected back after login
public static void ensureUserIsLoggedIn(Request request, Response response) {
if (request.session().attribute("currentUser") == null) {
request.session().attribute("loginRedirect", request.pathInfo());
response.redirect(Path.Web.LOGIN);
}
};
}
It has four Routes/methods, serveLoginPage, handleLoginPost, handleLogoutPost, and ensureUserIsLoggedIn. This is all the functionality that is related to login/logout. The serveLoginPage Route inspects the request session and puts necessary variables in the view model (did the user just log out? is there a uri to redirect the user to after login?), then renders the page. The ensureUserIsLoggedIn method is used in other controllers, for example in BookController:
public static Route fetchOneBook = (Request request, Response response) -> {
LoginController.ensureUserIsLoggedIn(request, response);
if (clientAcceptsHtml(request)) {
HashMap<String, Object> model = new HashMap<>();
Book book = bookDao.getBookByIsbn(getParamIsbn(request));
model.put("book", book);
return ViewUtil.render(request, model, Path.Template.BOOKS_ONE);
}
if (clientAcceptsJson(request)) {
return dataToJson(bookDao.getBookByIsbn(getParamIsbn(request)));
}
return ViewUtil.notAcceptable.handle(request, response);
};
The method intercepts the current Route fetchOneBook and redirects the user to the login page (if the user is not logged in). The origin path is stored in the sessions in ensureUserIsLoggedIn, so the user is redirected back to the correct place after login.
Response types
The fetchOneBook above controller gives three different answers based on the HTTP accept header: Try first to return HTML, then try to return JSON, finally return not-acceptable (this Route only produces HTML and JSON).
Localization
Localization in Java is pretty straightforward. You create two properties files with different suffixes, for example messages_en.properties (english) and messages_de.properties (german), then you create a ResourceBundle:
ResourceBundle.getBundle("localization/messages", new Locale("en"));
The setup is a bit more elborate if you clone the application (I created a small wrapper object with two methods), but the basics are extremely simple, and only uses native Java.
Rendering views
Rendering views is taken care of by another static helper, the ViewUtil:
public class ViewUtil {
public static String render(Request request, Map model, String templatePath) {
model.put("msg", new MessageBundle(getSessionLocale(request)));
model.put("currentUser", getSessionCurrentUser(request));
model.put("WebPath", Path.Web.class); // Access application URLs from templates
return strictVelocityEngine().render(new ModelAndView(model, templatePath));
}
}
The render method needs access to the request to check the locale and the current users. It puts this information in the template-model to ensure that the views are rendered correctly.
The ViewUtil also has some Route fields, such as notFound and notAcceptable. It’s a good place to put non-controller-specific error handling.
Example view
This code snippet shows the view for the fetchOneBook Route, which displays one book:
#parse("/velocity/layout.vm")
#@mainLayout()
#if($book)
<h1>$book.getTitle()</h1>
<h2>$book.getAuthor()</h2>
<img src="$book.getLargeCover()" alt="$book.getTitle()">
#else
<h1>$msg.get("BOOKS_BOOK_NOT_FOUND")</h1>
#end
#end
If the book is present, display it. Else, show a localized message saying the book was not found by using the msg object that was put into the viewmodel in the render() method above. The view also uses a layout template @#mainLayout() which is the page frame (styles, scripts, navigation, footer, etc.).
Conlusion
Hopefully you’ve learned a bit about Spark, and also Java and webapps in general. If you disagree with any choices made in the example-app, please create an issue on GitHub. This example will hopefully continue to evolve based on feedback and new Spark features.