This is the simplest fastest full fledged web server we could come up with.
Fluent-http is a very capable web stack based on SimpleFramework HTTP server.
Its goal is to provide everything a java web developer needs to build modern web sites with REST back-ends and HTML5 front-ends.
Simple rules are used to develop fluent-http and we believe it's what makes it a pleasure to use:
- Starting a web server should be a one-liner
- It should start at the speed of light
- It should rely on a robust, fast http server
- Using fluent-http should not imply using dozens of plugins and dependencies
- Web standards should be baked-in
java-11
A single dependency is what it takes. Release versions are deployed on Maven Central:
<dependency>
<groupId>net.code-story</groupId>
<artifactId>http</artifactId>
<version>2.105</version>
</dependency>Starting a web server that responds Hello World on / uri is as simple as:
import net.codestory.http.WebServer;
public class HelloWorld {
public static void main(String[] args) {
new WebServer().configure(routes -> routes.get("/", "Hello World")).start();
}
}What this code does:
- It starts a web server on port
8080 - To every
GETrequests on/, it will respondHello Worldastext/html - To every other request it will respond a nice
404error - It serves everything in
$CURRENT_DIR/appfolder as static resources
Not too bad for a one-liner, right?
Adding more routes is not hard either. It's based on Java 8 Lambdas:
new WebServer().configure(routes -> routes
.get("/", "Hello World")
.get("/Test", (context) -> "Other Test")
.url("/person")
.get((context) -> new Person())
.post((context) -> {
Person person = context.extract(Person.class);
// Do something
return Payload.created();
})
.url("/company")
.get((context) -> new Company())
.post((context) -> {
Company company = context.extract(Company.class);
// Do something
return Payload.created();
})
).start();Routes can have path parameters:
routes.get("/hello/:who", (context, who) -> "Hello " + who));
routes.get("/add/:first/to/:second", (context, first, second) -> Integer.parseInt(first) + Integer.parseInt(second));Routes can also have query parameters:
routes.get("/hello?who=:who", (who) -> "Hello " + who));
routes.get("/hello?to=:to&from=:from", (to, from) -> "Hello " + to + " from " + from));Notice that path and query parameters have to be of type String.
To overcome this limitation, fluent-http can be configured with
Resource classes instead of simple lambdas.
To sum up:
- The simpler syntax (
lambdas) is very easy to read but comes with limitations. - The more complex syntax (
Resource classes) has no such limitation and is very natural to people used toSpring MVCorJersey.
...
routes.add(new PersonResource());
routes.add("calculation", new CalculationResource());
...
@Prefix("/person")
public class PersonResource {
@Post("/")
public void create(Person person) {
// Do something
}
@Put("/:id")
public Payload update(String id, Person person) {
return new Payload(201);
}
@Get("/:id")
public Person find(String id, Context context, Headers headers, Request request, Response response, Cookies cookies, Query query, User user) {
Person person = ...
return NotFoundException.notFoundIfNull(person);
}
}
public class CalculationResource {
@Get("/add/:first/to/:second")
public int add(int first, int second) {
return first + second;
}
}Each method annotated with @Get, @Head, @Post, @Put, @Options or @Delete is a route.
The method can have any name. The parameters must match the uri pattern.
Parameters names are not important but it's a good practice to match the uri placeholders.
The conversion between path parameters and method parameters is done with
Jackson.
We can also let the web server take care of the resource instantiation. It will create a singleton for each resource, and recursively inject dependencies as singletons. It's a kind of poor's man DI framework, and be assured that your favourite DI framework can be plugged in also.
routes.add(CalculationResource.class);Resources can read values from header parameters. To do this, use context.header("headerName"):
@Get("tokenFromHeader")
public User user(Context context) {
return new Payload(context.header("token")).withCode(HttpStatus.OK);
}Before we take an in-depth look at fluent-http, you can go take a look at samples here if it's how you prefer to learn.
By default, fluent-http runs in development mode.
- It reloads the routes each time something changes in
appfolder - It provides
.mapand.sourcefor coffee and less files - It serves livereload protocol to refresh the browser on each static file change
In production mode:
- Stops looking for changes in
appfolder - Doesn't serve
.mapand.sourcefiles - It caches static resources as much as possible in memory
- It activates gzip compression on every request
We encourage you to use production mode whenever you deploy you website in real life and not activate it in dev mode.
To activate production mode, start the JVM with -DPROD_MODE=true.
When a web server is started, it automatically treats files found in app folder as static pages. The app folder
is searched first on the classpath (think src/main/resources/app) and then in the working directory.
So the simplest way to start a web server is in fact:
import net.codestory.http.WebServer;
public class HelloWorld {
public static void main(String[] args) {
new WebServer().start();
}
}Instead of relying on the default port, you can specify a port yourself. (Not sure anyone does this anymore thanks to Docker containers.)
new WebServer().start(4242);... or you can also let the web server find a tcp port available.
int port = new WebServer().startOnRandomPort().port();This is specially helpful for integration tests running in parallel. Note that the way it finds a port available is bulletproof on every OS. It chooses a random port, tries to start the web server and retries with a different port in case of error. This is much more reliable than the usual technique that relies on:
ServerSocket serverSocket = new ServerSocket(0);
int port = serverSocket.getLocalPort();
serverSocket.close();Fluent-http recognizes html files but not only. It is also able to transform more user-friendly file formats on the fly:
- Html (
.html) - Markdown (
.mdor.markdown) -> Compiled transparently to .html - Xml (
.xml) - Json (
.json) - Css (
.css) - Less (
.less) -> Compiled transparently to .css - Javascript (
.js) - Coffeescript (
.coffeeorlitcoffee) -> Compiled transparently to .js - Zip (
.zip) - Gz (
.gz) - Pdf (
.pdf) - Gif (
.gif) - Jpeg (
.jpegorjpg) - Png (
.png)
All those file formats are served without additional configuration. Files are served with automatic content-type, etag and last-modified headers. And you can override them of course.
Fluent-http supports WebJars to serve static assets.
Just add a maven dependency to a WebJar and reference the static resource in your pages with the /webjars/ prefix.
Here's an example with Bootstrap:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.2</version>
</dependency><!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="/webjars/bootstrap/3.3.2/css/bootstrap.min.css">
<script src="/webjars/bootstrap/3.3.2/js/bootstrap.min.js"></script>
</head>
<body>
<p>Hello World</p>
</body>
</html>Or better yet, use the [[webjar]] handlebars helper to set the version dynamically:
<!DOCTYPE html>
<html lang="en">
<head>
[[webjar bootstrap.min.js]]
</head>
</html>The [[webjar]] helper also supports a list as parameter:
---
bootstrapAssets: [
bootstrap.min.css,
bootstrap.min.js
]
---
<!DOCTYPE html>
<html lang="en">
<head>
[[webjar bootstrapAssets]]
</head>
</html>Static pages can use Yaml Front Matter as in Jekyll. For example this index.md
file:
---
greeting: Hello
to: World
---
[[greeting]] [[to]]Will be rendered as:
<p>Hello World</p>Take a look at Jekyll to understand the full power of Yaml Front Matter.
It makes it very easy to build static pages without duplication.
To make Yaml Front Matter even more useful, static pages can use HandleBar template engine.
---
names: [Doc, Grumpy, Happy]
---
[[#each names]]
- [[.]]
[[/each]]Will be rendered as:
<ul>
<li><p>Doc</p>
</li>
<li><p>Grumpy</p>
</li>
<li><p>Happy</p>
</li>
</ul>Handlebars syntax can be used in .html or .md files. You can
use the built-in helpers or add
your own helpers.
Note that because our stack is meant to be used with js frameworks like
AngularJs, we couldn't stick with standard {{}} handlebars notation.
We use the [[]] syntax instead, It makes it possible to mix server-side templates
with client-side templates on the same page.
Like in Jekyll, pages can be decorated with a layout. The name of the layout should be configured
in the Yaml Front Matter section.
For example, given this app/_layouts/default.html file:
<!DOCTYPE html>
<html lang="en">
<body>
[[body]]
</body>
</html>and this app/index.md file:
---
layout: default
---
Hello WorldA request to / will give this result:
<!DOCTYPE html>
<html lang="en">
<body>
<p>Hello World</p>
</body>
</html>A layout file can be a .html, .md, .markdown or .txt file. It should be put in app/_layouts folder.
The layout name used in the Yaml Front Matter section can omit the layout file extension.
Layouts are recursive, ie a layout file can have a layout.
A decorating layout can use variables defined in the rendered file. Here's an example with an html title:
<!DOCTYPE html>
<html lang="en">
<head>
<title>[[title]]</title>
</head>
<body>
[[body]]
</body>
</html>and this app/index.md file:
---
title: Greeting
layout: default
---
Hello WorldA request to / will give this result:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Greeting</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>In addition to the variables defined in the Yaml Front Matter section, some site-wide variables are available.
- All variables defined in the
app/_config.ymlfile, - The handlebars
site.datavariable is a map of every file inapp/_data/parsed and indexed by its file name (without extension), site.pagesis a list of all static pages inapp/. Each entry is a map containing all variables in the file's YFM section, pluscontent,pathandnamevariables.site.tagsis a map of every tag defined in static pages YMF sections. For each tag, there's the list of the pages with this tag. Pages can have multiple tags.site.categoriesis a map of every category defined in static pages YMF sections. For each category, there's the list of the pages with this category. Pages can have one or zero category.
Ok, so its easy to mimic the behaviour of a static website generated with Jekyll. But what about dynamic pages? Turns it's easy too.
Let's create a hello.md page with an unbound variable.
Hello [[name]]If we query /hello, the name will be replaced with an empty string since nowhere does it say what its value is. The solution is to override the default route to /hello as is:
routes.get("/hello", Model.of("name", "Bob"));Now, when the page is rendered, [[name]] will be replaced server-side with Bob.
If not specified, the name of the page (ie. the view) to render for a given uri is guessed after the uri. Files are
looked up in this order: uri, uri.html, uri.md, uri.markdown then uri.txt. Most of the time
it will just work, but the view can of course be overridden:
routes.get("/hello/:whom", (context, whom) -> ModelAndView.of("greeting", "name", whom));A route can return any Object, the server will guess the response's type:
java.lang.Stringis interpreted as inline html with content typetext/html;charset=UTF-8.byte[]is interpreted asapplication/octet-stream.java.io.InputStreamis interpreted asapplication/octet-stream.java.io.Fileandjava.nio.file.Pathare interpreted as a static file. The content type is guessed from the file's extension.java.net.URLis interpreted as a classpath resource. The content type is guessed from the resource's extension.Modelis interpreted as a template which name is guessed, rendered with given variables. The content type is guessed from the file's extension.ModelAndViewis interpreted as a template with given name, rendered with given variables. The content type is guessed from the file's extension.voidis empty content.- any other type is serialized to json, using
Jackson, with content typeapplication/json;charset=UTF-8.
For a finer control over the response of a route, one can return a Payload object rather than an Object.
Using a payload, one can set headers, cookies, content type and actual response.
routes.get("/hello", (context) -> Payload.ok());
routes.get("/hello", (context) -> Payload.seeOther("/anotherUri"));
routes.get("/hello", (context) -> new Payload("Hello"));
routes.get("/hello", (context) -> new Payload("text/html", "Hello", 200));
routes.get("/hello", (context) -> new Payload("text/html", "Hello", 200).withCookie("key", "value"));
...Cookies can be read on the request and sent on the response.
To read the cookies, you either ask through the Context (mainly for lambda routes).
routes.get("/hello", (context) -> Payload.cookies().value("name"));
routes.get("/hello", (context) -> Payload.cookies().value("name", "default"));
routes.get("/hello", (context) -> Payload.cookies().value("name", 42));
routes.get("/hello", (context) -> Payload.cookies().value("user", User.class));
routes.get("/hello", (context) -> Payload.cookies().keyValues());Or, for annotated resources, directly get an instance of Cookies injected.
public class MyResource {
@Post("/uri")
public void action(Cookies cookies) {
String value = cookies.value("name", "Default value");
...
}
}To add a cookie to a response, use a withCookie(...) method on the Payload:
return new Payload(...).withCookie("key", "value");
return new Payload(...).withCookie("key", 42);
return new Payload(...).withCookie("key", new Person(...));
...Now that the website is dynamic, we might also want to post data. We support GET, POST, PUT and DELETE methods.
Here's how one would support posting data:
routes.post("/person", context -> {
String name = context.query().get("name");
int age = context.query().getInteger("age");
Person person = new Person(name, age);
// do something
return Payload.created();
});It's much easier to let Jackson do the mapping between form parameters and Java Beans.
routes.post("/person", context -> {
Person person = context.extract(Person.class);
// do something
return Payload.created();
});Using the annotated resource syntax, it's even simpler:
public class PersonResource {
@Post("/person")
public void create(Person person) {
repository.add(person);
}
}Multiple methods can match the same uri:
public class PersonResource {
@Get("/person/:id")
public Model show(String id) {
return Model.of("person", repository.find(id));
}
@Put("/person/:id")
public void update(String id, Person person) {
repository.update(person);
}
}Same goes for the lambda syntax:
routes
.get("/person/:id", (context, id) -> Model.of("person", repository.find(id)))
.put("/person/:id", (context, id) -> {
Person person = context.extract(Person.class);
repository.update(person);
return Payload.created();
});
}Or to avoid duplication:
routes
.url("/person/:id")
.get((context, id) -> Model.of("person", repository.find(id)))
.put((context, id) -> {
Person person = context.extract(Person.class);
repository.update(person);
return Payload.created();
});
}Etag headers computation is automatic on every query. Each time a non streaming response is made by the server,
etag headers are added automatically. It's performed by the default PayloadWriter implementation.
Each time a query is made with etag headers, the server can decide whether to return a 302 Not Modified code
or not.
If you want to implement a different etag strategy, you have to provide your own implementation of PayloadWriter.
routes.setExtensions(new Extensions() {
public PayloadWriter createPayloadWriter(Request request, Response response, Env env, Site site, Resources resources, CompilerFacade compilers) {
return new PayloadWriter(request, response, env, site, resources, compilers) {
protected String etag(byte[] data) {
return Md5.of(data);
}
...
};
}));Starting the web server in SSL mode is very easy. You need a certificate file (.crt) and a private key file (.der),
That's it. No need to import anything in a stupid keystore. It cannot be easier!
new WebServer().startSSL(9443, Paths.get("server.crt"), Paths.get("server.der"));It is also possible to use a TLS certificate chain with intermediate CA certificates:
new WebServer().startSSL(9443, Arrays.asList(Paths.get("server.crt"), Paths.get("subCA.crt")),
Paths.get("server.der")
);When an authentication with a client certificate is required, it is possible to specify a list of accepted trust anchor certificates:
new WebServer().startSSL(9443, Arrays.asList(
Paths.get("server.crt"), Paths.get("subCA.crt")),
Paths.get("server.der"),
Arrays.asList(Paths.get("trustAnchor.crt"))
);Note that, as of now, we only accept rsa key file in PKCS#8 format in a .der binary file.
You have probably noticed, fluent-http comes with pre-packaged, kitten-ready" 404 & 500 error pages.
If you want to customize these pages or are member of the CCC "Comité Contre les Chats", just put a 404.html or 500.html at the root of your app folder and they will be served instead of the defaults.
Json is supported as a first class citizen. Producing json is as easy as this:
routes.get("/product", () -> new Product(...));
routes.get("/products", () -> Arrays.asList(new Product(...), new Product(...)));These routes serve the Products serialized as json using Jackson.
The content type will be application/json;charset=UTF-8.
When fluent-http talks json, the jackson json processor is used.
Sometimes (meaning: always in any decent sized project), you want to provide your own home-cooked ObjectMapper.
You can do this by configuring or replacing the ObjectMapper through the Extensions interface.
routes.setExtensions(new Extensions() {
@Override
public ObjectMapper configureOrReplaceObjectMapper(ObjectMapper defaultObjectMapper, Env env) {
defaultObjectMapper.registerModule(new JSR310Module())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return defaultObjectMapper;
}
});Cross-cutting behaviours can be implemented with filters. For example, one can log every request to the server with this filter:
routes.filter((uri, context, next) -> {
System.out.println(uri);
return next.get();
})A filter can be defined in its own class:
routes.filter(LogRequestFilter.class);
public class LogRequestFilter implements Filter {
@Override
public Payload apply(String uri, Context context, PayloadSupplier next) throws Exception {
System.out.println(uri);
return next.get();
}
}A filter can either pass to the next filter/route by returning next.get() or it can totally bypass the chain of
filters/routes by returning its own Payload. For example, a Basic Authentication filter would look like:
public class BasicAuthFilter implements Filter {
private final String uriPrefix;
private final String realm;
private final List<String> hashes;
public BasicAuthFilter(String uriPrefix, String realm, Map<String, String> users) {
this.uriPrefix = uriPrefix;
this.realm = realm;
this.hashes = new ArrayList<>();
users.forEach((String user, String password) -> {
String hash = Base64.getEncoder().encodeToString((user + ":" + password).getBytes());
hashes.add("Basic " + hash);
});
}
@Override
public Payload apply(String uri, Context context, PayloadSupplier nextFilter) throws Exception {
if (!uri.startsWith(uriPrefix)) {
return nextFilter.get(); // Ignore
}
String authorizationHeader = context.getHeader("Authorization");
if ((authorizationHeader == null) || !hashes.contains(authorizationHeader.trim())) {
return Payload.unauthorized(realm);
}
return nextFilter.get();
}
}Both BasicAuthFilter and LogRequestFilter are pre-packaged filters that you can use in your applications.
Out of the box support for Guice: just throw in your Guice dependency in your pom, and you're ready to roll.
Let's say you got some Guice Module like this.
public class ServiceModule extends AbstractModule {
@Override
protected void configure() {
bind(MongoDB.class);
bind(AllProducts.class);
bind(AllOrders.class);
bind(AllEmails.class);
}
}Wiring them in can be done in your WebConfiguration like this
public void configure(Routes routes) {
routes.setIocAdapter(new GuiceAdapter(new ServiceModule()));
routes.get("/foobar", "<h1>FOO BAR FTW</h1>");
}Now you can inject your beans like you would expect
public class AllProducts {
private final MongoCollection products;
@Inject
public AllProducts(MongoDB mongodb) {
products = mongodb.getJongo().getCollection("product");
}We support Spring injected bean in exactly the same manner as with guice. Check the SpringAdapter class, which works the same way as its Guice counterpart.
Look at the SpringAdapterTest we wrote for a working example.
You'll probably sooner than later want to add to some custom HandleBars helpers for your server-side templates.
You first start to write you own helper in Java.
import com.github.jknack.handlebars.Helper;
import com.github.jknack.handlebars.Options;
.../...
public enum HandleBarHelper implements Helper<Object> {
appStoreClassSuffix {
@Override
public CharSequence apply(Object context, Options options) throws IOException {
if (((String) context).contains("google"))
return "_play";
else
return "_ios";
}
},
.../...
}You wire it in by adding your helper class to the CompilersConfiguration by the way of the Extensions interface.
routes
.setExtensions(new Extensions() {
@Override
public void configureCompilers(CompilersConfiguration compilers, Env env) {
compilers.addHandlebarsHelpers(HandleBarHelper.class);
compilers.addHandlebarsHelpers(AnotherHelper.class);
}
})You are able to use your own helper in any of your template like this example for the code above.
<a href="[[appStoreUrl]]" class="btn_appstore[[appStoreClassSuffix appStoreUrl]]"></a>fluent-http comes with some build-in handlebars helpers, to make your life easier:
livereloadwhich provides the livereload client-side js script.[[livereload]]is already injected in thedefaultlayout.GoogelAnalyticswhich provides client-side injection of the google analytics script. Use it like that:[[google_analytics 'UA-XXXXXX-X']]StringHelperwhich provides[[capitalizeFirst]],[[lower]],[[stringFormat ]]check the documentation- TODO:
css,script
fluent-http uses a disk cache to store .css and .js files produced from .less or .coffee files.
This directory is by default stored in your "user home" in a .code-story directory.
If you don't want it to be here you can -Duser.home=/foo/bar as you see fit.
If you're paranoid and run fluent-http under nobody user make sure nobody can read/write this directory.
- Configure the threads of the underlying
SimpleHttpServer: ThreadConfigurationSample.java
mvn license:formatmvn clean verifyBuild the release:
mvn release:clean release:prepare release:perform