Soklet
What Is It?
Minimalist infrastructure for Java webapps and microservices.
Design Goals
- Single focus, unopinionated
- No external servlet container required
- Fast startup, under a second on modern hardware
- No 3rd party dependencies - uses only standard JDK APIs
- Extensible - applications can easily hook/override core functionality via DI
- Self-contained deployment (single zip file)
- Static resource filename hashing for efficient HTTP caching and versioning
- Java 8+, Servlet 3.1+
Design Non-Goals
- Dictation of what libraries and versions to use (GSON vs. Jackson, Mustache vs. Velocity, etc.)
- Baked-in authentication and authorization
- Database support (you can bring your own with Pyranid)
License
Maven Installation
com.sokletsoklet1.2.7
Direct Download
If you don’t use Maven, you can drop soklet-1.2.7.jar directly into your project. You’ll also need javax.inject-1.jar and javax.servlet-api-3.1.0.jar as dependencies.
App Startup
Soklet applications are designed to launch via public static void main()
, just like a regular Java application. You do not have to worry about environment and external server setup, deployment headaches, and tricky debugging.
// Assumes you're using Guice as your DI framework via soklet-guicepublic static void throws Exception {Injector injector = Guice.;Server server = injector.;// Start the servernew ServerLauncher(server).;}
Resource Methods
Soklet’s main job is mapping Java methods to URLs. We refer to these methods as resource methods.
Resource methods may return any type, such as String
or UUID
, but normally you’ll return special types like PageResponse
and ApiResponse
.
Example Code
// Any class containing URL-resource methods must have the @Resource annotation applied.// This is a performance optimization for fast startup time. Soklet uses an annotation processor// at compile time to create a lookup table which avoids runtime reflection@Resource
Resource Method Return Types
There are 5 standard resource method return types provided by Soklet.
ApiResponse
Holds an arbitrary object that is meant to be written as an “API” response (often JSON or XML)BinaryResponse
Designed for writing arbitrary content to the response, e.g. streaming a PDFCustomResponse
Indicates Soklet should take no action - you are responsible for writing the response yourselfPageResponse
Holds a logical page template name and optional model data to merge with it, meant to be written as an HTML page response. Some popular templating technologies are Velocity, Freemarker, and MustacheRedirectResponse
Performs standard 301 and 302 redirects
Returning void
or null
will result in a 204
with an empty response body.
Returning types other than those listed above (e.g. UUID
or Double
or MyCustomType
) will invoke Soklet’s default behavior of writing their toString()
value to the response with content type text/plain;charset=UTF-8
.
Response Writers
You might implement a Mustache.java PageResponseWriter
like this:
You might implement a Jackson ApiResponseWriter
like this:
Error Handling
When an exception is thrown by a resource method, it’s up to your ExceptionStatusMapper
to determine the appropriate HTTP status code and your ResponseWriter
implementations to figure out how to communicate details back to the user (for example, render a custom error page or a special JSON for your API).
Standard Exception Types
Soklet provides these exceptions out of the box, but any exception your code throws will work. By default, other exception types will return a 500 status, but you can customize this behavior - see the Customizing Status Codes section below.
BadRequestException
- 400AuthenticationException
- 401AuthorizationException
- 403NotFoundException
- 404MethodNotAllowedException
- 405
Example Resource Method
// Example URL: /users/ba19be82-5d90-4b3b-b78f-284c5b86ae11public ApiResponse {Optional<User> user = userService.;if(!user.)throw new NotFoundException();if(user.. && !currentContext.)throw new MyCustomException("You can't see this top-secret user!");return new ApiResponse(user);}
Customizing Status Codes
public static void throws Exception {Injector injector = Guice.;Server server = injector.;new ServerLauncher(server).;}
App Configuration
Server Setup
There’s no need for a web.xml
file. Your server is configured in code. You just need to pick a Server
implementation.
- Jetty support is provided by soklet-jetty
- Experimental Tomcat support is provided by soklet-tomcat
Jetty is recommended unless you have special requirements.
public static void throws Exception {Injector injector = Guice.;Server server = injector.;new ServerLauncher(server).;}
WebSockets
Oracle provides a nice explanation of WebSockets in its WebLogic documentation. Here’s an important quote:
As opposed to servlets, WebSocket endpoints are instantiated multiple times. The container creates one instance of an endpoint for each connection to its deployment URI. Each instance is associated with one and only one connection. This behavior facilitates keeping user state for each connection and simplifies development because only one thread is executing the code of an endpoint instance at any given time.
Like Servlets and Filters, Soklet will use your dependency injection library to provide WebSocket instances. All you have to do is build your WebSockets using standard JSR-356 annotations like @ServerEndpoint
, @OnOpen
, @OnMessage
, @OnClose
, and @OnError
. The @ServerEndpoint
annotation is required for the WebSocket to function.
A common implementation pattern is for a WebSocket to listen for events from some other system component using a Listener pattern or event bus and, when system state changes, data is written to the client.
// Other imports elided// Example of a WebSocket that listens for events from the backend// and sends notifications down to the client.
It is important to be careful of memory leaks. Suppose your backend maintains a collection of strong references to its WebSocket Listeners. If your WebSockets don’t deregister themselves correctly, they will never be deallocated. A good strategy here is to store listeners using weak references, like this:
// Other imports elided@Singleton
Interceptors
Resource method interceptors are an alternative to traditional Servlet Filters. DI frameworks normally provide interceptor functionality on which you can build.
Examples of common interceptors follow. Note that Soklet does not provide any interceptors, database access, or security features out of the box - these examples are for illustration only.
public static void throws Exception {Injector injector = Guice.;Server server = injector.;new ServerLauncher(server).;}// Guice interceptor that wraps each resource method in a database transaction// Guice interceptor that performs security checks
Deployment Archives
During development, you will normally launch a Soklet application via Maven or your IDE. For test and production builds, you’ll want to create a deployment archive. This archive is a self-contained zip file which only requires Java 1.8 to run - no dependency on an external server, Maven, or any other 3rd party package.
Soklet provides an Archiver
, which allows you specify how to construct the zip file, similar to an Ant script. Archiver
exposes customization hooks to give you fine-grained control over how to build your archive.
The difference between archiving and just running an app is that archiving is a great opportunity to perform additional time-consuming work that you don’t normally want to do during development. Some common examples are:
- Compressing/combining JS and CSS files
- Hashing static resources (handled by Soklet; see Hashed Files section below)
- Pre-gzipping static resources for efficient serving (handled by Soklet)
Note that archiving is done in a temporary sandbox directory, so your current working directory is untouched.
public static void throws Exception {// Archive file to createPath archiveFile = Paths.;// Copy these directories and files into the archive.// If you specify a directory, its contents are copied to the destination.// If you specify a single file, it is copied to the destination.// If no destination is specified, it is assumed to be identical to the sourceSet<ArchivePath> archivePaths = new HashSet<ArchivePath>();// The archiver will create copies of static files, embedding a hash of the file contents// in the filename. See "Hashed Files" section for more about thisStaticFileConfiguration staticFileConfiguration =StaticFileConfiguration...;// You may optionally alter files in-place - for example, here we compress JS and CSS files.// The Archiver works in its own sandbox, so any alterations performed are written// to a temporary file, leaving the original untouchedFileAlterationOperation fileAlterationOperation = (archiver, workingDirectory, file) -> {String filename = file...;// Compression implementations are left to your imaginationif (filename.)return Optional.;if (filename.)return Optional.;// Returning empty means the file should not be alteredreturn Optional.;};// Maybe we use grunt to do some extra build-time processing (LESS -> CSS, for example).// You can launch arbitrary processes using ArchiverProcessArchiveSupportOperation preProcessOperation = (archiver, workingDirectory) -> {// The working directory is Soklet's temporary archive-building sandbox directorynew ArchiverProcess("/usr/local/bin/grunt", workingDirectory).;new ArchiverProcess("/usr/local/bin/grunt", workingDirectory).;};// Build and run our Archiver.// Specifying 'mavenSupport()' here means standard Maven clean, compile,// and dependency goals are used as part of the archiving process.// If you don't use Maven, it's your responsibility to compile your code// and include dependency JARs in the archiveArchiver archiver = Archiver.......;archiver.;}
Hashed Files
Soklet’s archive process will create copies of your static files and embed a content-based hash in the copied filename. Further, a manifest is created which maps original URL paths to hashed URL paths for use at runtime.
For example
static/js/jquery.js
Might have a hashed copy like
static/js/jquery.D1F585EEEC4308D432181FF88068830A.js
Why Is This Important?
The hashing process and corresponding manifest is useful because:
- You never have to worry about browsers using outdated files in a local cache
- You can send “cache forever” HTTP headers when serving files with embedded hashes
- Hashing by file content ensures browser cache misses only occur when files themselves change, in contrast to other strategies like
/file?version=1.1
- Embedding hash in filename instead of as a query parameter can result in better proxy performance
Manifest Creation
The archive process will create a hashed URL manifest file at the root of the archive named hashedUrlManifest
.
Its format is not formally defined and is subject to change, but for illustration purposes it might look like this:
You can use it in Java code like this:
// Default ctor loads manifest from file named ```hashedUrlManifest``` in working directoryHashedUrlManifest hashedUrlManifest = new HashedUrlManifest();Optional<String> hashedUrl = hashedUrlManifest.;// Output is "Optional[/static/js/jquery.D1F585EEEC4308D432181FF88068830A.js]"out.;String failsafeHashedUrl = hashedUrlManifest.;// Output is "/static/js/fake.js"out.;
Archiving also creates a JavaScript version of the manifest (configured to be /static/js/hashed-urls.js
above), useful for when JavaScript must load up static resources - for example, creating an img
tag in code, or dynamically loading a script.
The content of the JavaScript version of the manifest might look like this:
soklethashedUrls ="/static/images/cartoon.png": "/static/images/cartoon.D958A21CF25246CA0ED6AA8BF0B1940E.png""/static/js/jquery.js": "/static/js/jquery.D1F585EEEC4308D432181FF88068830A.js""/static/js/hashed-urls.js": "/static/js/hashed-urls.5EA2BAEF93DA17B978E20E0507A1F56E.js";
Hashing Example: HTML templates
If you use Mustache.java to render your HTML, you might configure it to support hashed URLs as follows:
// Define a custom Mustache TemplateFunctionmodel.;// Later on...Mustache mustache = mustacheFactory.;mustache..;
Your Mustache markup might look like this:
<link href="{{#hashedUrl}}/static/css/my-app.css{{/hashedUrl}}" type="text/css" rel="stylesheet" />
…and then at runtime:
<link href="/static/css/my-app.7EA2BAEF93DA17B978E20E0507A1F56E.css" type="text/css" rel="stylesheet" />
Hashing Example: API Responses
This server-side code
public ApiResponse {String baseUrl = "http://example.website.com";String imageUrl = baseUrl + hashedUrlManifest.;return new ApiResponse(new ArrayList<Map<String, Object>>());}
Might render
Hashing Example: JavaScript
myApp {var hashedUrl = soklethashedUrlsurl;return hashedUrl ? hashedUrl : url;};// Creates a tag like <img src='/static/images/cartoon.D958A21CF25246CA0ED6AA8BF0B1940E.png'/>;
CSS File Hashing
During the archive process, Soklet will automatically detect and rewrite references to hashed URLs in your CSS files.
For example, this CSS rule:
Might be rewritten to this:
Relative paths are automatically rewritten as well:
WARNING!
Currently, there are restrictions on CSS rewriting. They are:
- URLs cannot contain inner
..
and.
values. For example,../images/cartoon.png
is OK but../images/../cartoon.png
is not - CSS
@import
URLs should be avoided (Soklet will rewrite the URLs, but the hashes may be “stale” in cases where there are chains of imports, e.g. CSS file 1 imports CSS file 2 which imports CSS file 3)
Soklet will warn you if it detects either of these conditions.
java.util.logging
Soklet uses java.util.logging
internally. The usual way to hook into this is with SLF4J, which can funnel all the different logging mechanisms in your app through a single one, normally Logback. Your Maven configuration might look like this:
ch.qos.logbacklogback-classic1.1.9org.slf4jjul-to-slf4j1.7.22
Because it is such a common operation, Soklet provides an optional facility for configuring Logback. You might have code like this which runs at startup:
// Configures Logback; also bridges java.util.Logging callsLoggingUtils.;