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:
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:
8080
GET
requests on /
, it will respond Hello World
as text/html
404
error$CURRENT_DIR/app
folder as static resourcesNot 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:
lambdas
) is very easy to read but comes with limitations.Resource classes
) has no such limitation
and is very natural to people used to Spring MVC
or Jersey
....
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.
app
folder.map
and .source
for coffee and less filesIn production mode:
app
folder.map
and .source
filesWe 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
).md
or .markdown
) -> Compiled transparently to .html.xml
).json
).css
).less
) -> Compiled transparently to .css.js
).coffee
or litcoffee
) -> Compiled transparently to .js.zip
).gz
).pdf
).gif
).jpeg
or jpg
).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="https://github.com/CodeStory/fluent-http/blob/master/webjars/bootstrap/3.3.2/css/bootstrap.min.css">
<script src="https://github.com/CodeStory/fluent-http/raw/master/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 World
A 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 World
A 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.
app/_config.yml
file,site.data
variable is a map of every file in app/_data/
parsed and indexed by its file name (without extension),site.pages
is a list of all static pages in app/
. Each entry is a map containing all variables in the file's YFM section, plus content
, path
and name
variables.site.tags
is 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.categories
is 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.String
is interpreted as inline html with content type text/html;charset=UTF-8
.byte[]
is interpreted as application/octet-stream
.java.io.InputStream
is interpreted as application/octet-stream
.java.io.File
and java.nio.file.Path
are interpreted as a static file. The content type is guessed from the file's extension.java.net.URL
is interpreted as a classpath resource. The content type is guessed from the resource's extension.Model
is interpreted as a template which name is guessed, rendered with given variables. The content type is
guessed from the file's extension.ModelAndView
is interpreted as a template with given name, rendered with given variables. The content type is
guessed from the file's extension.void
is empty content.Jackson
, with content type application/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="https://github.com/CodeStory/fluent-http/blob/master/[[appStoreUrl]]" class="btn_appstore[[appStoreClassSuffix appStoreUrl]]"></a>
fluent-http
comes with some build-in handlebars helpers, to make your life easier:
livereload
which provides the livereload client-side js script. [[livereload]]
is already injected in the default
layout.GoogelAnalytics
which provides client-side injection of the google analytics script. Use it like that: [[google_analytics 'UA-XXXXXX-X']]
StringHelper
which provides [[capitalizeFirst]]
, [[lower]]
, [[stringFormat ]]
check the documentationcss
, 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.
SimpleHttpServer
: ThreadConfigurationSample.javamvn license:format
mvn clean verify
Build the release:
mvn release:clean release:prepare release:perform