Skip to content

krampenschiesser/strudel

Repository files navigation

Strudel micro web framework

Strudel is heavily inspired by the Sparkjava framework, well it is basically a copy. Why copy it?

  1. The static model was troublesome closing a lot of doors

  2. I had trouble extending it or changing configurations like TemplateEngines

  3. I wanted to play around with the cool kid undertow

  4. I always ended up adding basic features like decent Locale support and a RequestScope

Tip
All code snippets in this readme have working examples in the examples folder that you can run and view.

Hello World

It looks very similar to spark, the differences are:

  • you only work on an instance: strudel.

  • you are in control of starting and stopping the server

Strudel strudel = Strudel.create();
strudel.get("/", (request, response) -> "hello sauerland!");
strudel.start();

There is one specialty about the Strudel.create() method. It is actually creating a guice injector. So a manual setup would look like:

Injector injector = Guice.createInjector(new StrudelModule());
Strudel strudel = injector.getInstance(Strudel.class);
strudel.get("/", (request, response) -> "hello sauerland!");
strudel.start();
Tip
See example HelloWorld

What are the major differences between spark and strudel

Spark

Static singleton, only 1 instance per JVM.

Spark.get("/hello", (req,res) -> "hello");
Tip
Changed with Spark’s instance API in 2.5
Strudel

@Singleton/Soft singleton provided by guice, multiple instances in 1 JVM possible

    Strudel first = Strudel.create();
    Strudel second= Strudel.create();
    first.get("/", (request, response) -> "hello I am server 1");
    second.get("/", (request, response) -> "hello I am server 2");

    first.options().port(8000);
    second.options().port(8001);

    first.start();
    second.start();
Tip
See example MultipleInstances

Spark

Requests are always handled by separate thread. (Servlet based)

Strudel

Relying heavily on undertow I can dispatch tasks to be executed async while also being able to serve tasks synchronously in the same thread.

Handler handler = (request, response) -> {
  Thread.sleep(TimeUnit.SECONDS.toMillis(10));
  return "hello i am running in a worker thread";
};

strudel.get("/", handler).async();
strudel.get("/sync", (request, response) -> "hello i am running in the IO thread");

Spark

All methods only accept instances as parameters

Strudel

All methods accept instances or classes as parameters. Therefore you can use the full power of Dependency Injection to build your application. You decide if you want a new instance or a singleton handling the request. Or maybe you implement a custom guice scope that acts like a cache and can be cleared?

strudel.get("/", MyHandler.class);
Tip
See example SimpleDI

User guide

Simple routes

Strudel uses undertows RoutingHandler in order to map the routes. So you can use wildcards and route-parameters:

strudel.get("/get", (request, response) -> "get");
strudel.put("/put", (request, response) -> "put");
strudel.post("/post", (request, response) -> "post");
strudel.delete("/delete", (request, response) -> "delete");

strudel.get("/wild/*", (request, response) -> "Wildcard route: " + request.routeWildcard());

strudel.get("/user/{name}/page/{page}", (request, response) -> {
  String name = request.routeParameter("name");
  String page = request.routeParameter("page");
  return "Parameter route: user=" + name + ", page=" + page;
});
Tip
See example SimpleRoutes

Customizing routes

Each route returns a RouteBuilder that you can use to customize the behaviour of this route. Current customizations are:

  • async() to execute this route in a worker thread

  • sync() to execute this route in the IO thread

  • gzip() to zip the content

  • template() to mark the route as a template route

  • json() to return json from this route

What is sync/async

Undertow supports simple non blocking requests to be executed in a single thread called the IO Thread. Background/blocking work is submitted to worker threads which follows the same model as traditional servlet servers.

Tip
in fact there are multiple IO threads, but if you block one of them it is a mess

The following routes are asynchronous by default and run in worker threads:

  • PUT/POST because I need to enter blocking mode and read from the input stream

  • template routes

  • classpath routes

  • external folder routes

  • webjar routes

The following routes are synchronous and run in the IO thread:

  • GET/DELETE routes

Tip
See example AsyncGet

Filters

You can add filters that are executed before and after route calls:

strudel.before("/secure/*", (request, response) -> {
  if (!checkAuth(request)) {
    response.halt(HttpStatus.FORBIDDEN);
  }
});
strudel.get("/", (request, response) -> "i am the home");
strudel.get("/secure/panel", (request, response) -> "Secure region");

HandlerNoReturn before = (request, response) -> log.info("Before async execution");
HandlerNoReturn after = (request, response) -> log.info("After async execution");
strudel.get("/async", (request, response) -> "i am async").async(before, after);
Warning
There is one caveat here for async routes. Filters are always executed synchronous in the IO thread and will prevent an async route to be dispatched to a worker thread.

If you want to add callbacks for the async route you can use the method on async(before,after) on the RouteBuilder:

strudel.get("/async", (request, response) -> "i am async").async(before, after);
Tip
See example Filter

Redirects

Redirecting is simple and can be done via the Response:

strudel.get("/",(request, response) -> response.redirect("/target"));
strudel.get("/target", (request, response) -> "You were redirected");
Tip
See example Redirect

Gzip support

If you want a route to be compressed just configure it to be zipped:

String longString = IntStream.range(0, 1500).mapToObj(i -> "1").collect(Collectors.joining());
strudel.get("/", (request, response) -> "I am not zipped").gzip();
strudel.get("/zip", (request, response) -> longString + "<br/>\nI am zipped!").gzip();

Please note that only above a certain content-length (1480) I start to zip the content.

Tip
See example Gzip

Locale parsing

The locale of a request is resolved in 3 ways:

  1. I look if there is a query parameter lang. A request like this http://localhost/?lang=de will switch to german language

  2. I look for a cookie with the with the name lang and use its value as language

  3. I check for the Accept-Language Http-Header and use the main language

  4. If I still don’t have a locale, English is used

The first language returned by any of these 3 checks will be used. So as a developer you can quickly view a page in a different language. As a user you can have a cookie specifying your preferred language. As a visitor the page is shown to you with your browsers default language.

The locale is resolved with the class LocaleResolver feel free to replace it in your guice module with a custom implementation.

Tip
See example DefaultTemplateEngine

Reading PUT/POST body

Reading a put/post body is done via the Request:

strudel.post("/post", (request, response) -> "You submitted the following body: <br/>\n" + request.body());
Tip
See example Postbody

Reading FormData

Reading formdata is simple, too. Thanks alot to the great utils of undertow:

strudel.post("/post", (request, response) -> {
  String value = request.formData("text");
  return "You submitted value: <b>" + value + "</b>";
});
Tip
See example Formdata

Handling file uploads

Again this is reading formdata and is super simple. The following code needs a file upload and reflects the uploded bytes back to you.

strudel.post("/post", (request, response) -> {
  Path path = request.formDataFile("file");
  if (path == null) {
    return "No file given";
  } else {
    response.contentType(MediaType.ANY_IMAGE_TYPE.type());
    return Files.readAllBytes(path);
  }
});
Tip
See example Fileupload

Websockets

Websockets work via the undertow internal websocket api. This is not the greatest of them all but it works. I might wrap it in the future. However I do not want to use the JSR356 API sind I don’t want to use reflection to parse given classes.

Registering a websocket:

strudel.websocket("/echo", null, Listener::new);

The first argument is a listener that is called when the websocket is opened. You can use it to associate a channel with eg. a user. The second argument is a factory for the listener used on that specific channel. In our echo example we don’t need to handle the open of the connection. We just reflect incoming messages with our Listener:

static class Listener extends AbstractReceiveListener {
  @Override
  protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) throws IOException {
    WebSockets.sendText("Server says: " + message.getData(), channel, null);
  }
}
Tip
See example EchoServer

Webjars

Integration of webjars is very simple but not enabled by default:

strudel.webjars();

Json

A Rest endpoint providing json is made via calling the json() method on the RouteBuilder

strudel.get("/", (request, response) -> new MyPojo("Hans Wurst GSon", 42)).json();
strudel.get("/jackson", (request, response) -> new MyPojo("Hans Wurst Jackson", 42)).json(JacksonParser.class);

You can even specify which json engine to use (Gson for small answers, Jackson for big answers). Currently there are 2 json parsers:

  • GSon

    compile "de.ks.strudel:strudel-json-gson:$strudelversion"
  • Jackson

    compile "de.ks.strudel:strudel-json-jackson:$strudelversion"
Tip
See example RestServer

Consuming JSON is also easy. You can always inject the parser itself and go from there:

MyHandler.java
@Inject
JsonParser parser;

public void parse(String input) {
    MyPojo object = parser.fromString(input, MyPojo.class);
    ...
}

Or if you just have the simple case of turning the message body into an object:

MyPojo myPojo = request.bodyFromJson(MyPojo.class);

Request scope

I implemented a request scope that lets you inject the current Request, Response and Locale into your beans.

Tip
See example RequestScopeExample

Https

Starting undertow with https is also pretty simple There are 2 ways to do this:

  1. Use the options().secure method to create a sslcontext

    strudel.options().secure("/secure/keystore.jks", "password");
  2. Create your own guice provider of SSLContext

    javax.net.ssl.SSLContext
Tip
See example HttpsExample

Template enines

Strudel has build in support for multiple template engines:

  • freemarker

    compile "de.ks.strudel:strudel-template-freemarker:$strudelversion"
  • handlebars

    compile "de.ks.strudel:strudel-template-handlebars:$strudelversion"
  • jade

    compile "de.ks.strudel:strudel-template-jade:$strudelversion"
  • mustache

    compile "de.ks.strudel:strudel-template-mustache:$strudelversion"
  • pebble

    compile "de.ks.strudel:strudel-template-pebble:$strudelversion"
  • thymeleaf (3.0)

    compile "de.ks.strudel:strudel-template-thymeleaf:$strudelversion"
  • trimou

    compile "de.ks.strudel:strudel-template-trimou:$strudelversion"

I also would love to include rocker which is the fastest engine with a really nice approach. But sadly it is strongly based on maven and javaagents.

Running the template benchmark locally with recent versions I get the following results:

Benchmark Mode Cnt Score Error Units

Freemarker.benchmark

thrpt

50

17,244.626

± 311.420

ops/s

Mustache.benchmark

thrpt

50

22,999.379

± 290.057

ops/s

Pebble.benchmark

thrpt

50

32,607.491

± 795.512

ops/s

Rocker.benchmark

thrpt

50

41,433.193

± 1,164.793

ops/s

Thymeleaf.benchmark

thrpt

50

6,393.351

± 73.580

ops/s

Trimou.benchmark

thrpt

50

21,647.772

± 803.671

ops/s

Velocity.benchmark

thrpt

50

22,363.383

± 329.376

ops/s

So rocker is the fastest as it compiles its templates into bytecode.
However pebble is just blazingly fast without doing fancy tricks.

Localization support

The following template engines support localization:

  • Thymeleaf

    <h1 th:text="#{key}">No translation</h1>
  • Pebble

    <h1>{{ i18n("WEB-INF/template/index","key") }}</h1>
  • Handlebars (the variable locale below comes from the model and is automatically set by strudel)

    <h1>{{ i18n "key" bundle="WEB-INF/template/index" locale=locale }}</h1>
  • Trimou

    <h1>{{ i18n "key" }}</h1>

Using a template engine

There are 2 ways of using a template engine:

  1. create a binding for the interfae TemplateEngine to you preferred template engine implementation:

    bind(TemplateEngine.class).to(TrimouEngine.class);
    //rendering via:
    strudel.get("/", (request, response) -> {
      Map<String, String> model = new HashMap<>();
      model.put("title", "Hello Title!");
      model.put("hello", "Hello Sauerland!");
      return new ModelAndView(model, "trimouhello.html");
    }).template();
  2. Pass the template engine to specific routes (want to use different template engine for css?)

    strudel.get("/", (request, response) -> {
      Map<String, String> model = new HashMap<>();
      model.put("title", "Hello Title!");
      model.put("hello", "Hello Sauerland!");
      return new ModelAndView(model, "trimouhello.html");
    }).template(TrimouEngine.class);

There are some things that are common for using all of the template engines:

  • include the corresponding dependencies, eg:

    compile "de.ks:strudel-template-trimou:$strudelversion"
  • Create Strudel with an additional guice module (one for each template engine)

    Strudel strudel = Strudel.create(new TrimouModule());
  • create a handler that returns an instance of ModelAndView and configure it as a template route

    strudel.get("/", (request, response) -> {
      Map<String, String> model = new HashMap<>();
      model.put("title", "Hello Title!");
      model.put("hello", "Hello Sauerland!");
      return new ModelAndView(model, "trimouhello.html");
    }).template();

Putting it together

public class Templating {
  public static void main(final String[] args) {
    Strudel strudel = Strudel.create(new TemplateModule(), new TrimouModule("WEB-INF/template/localization"));
    strudel.get("/", (request, response) -> {
      Map<String, String> model = new HashMap<>();
      model.put("title", "Hello Title!");
      model.put("hello", "Hello Sauerland!");
      return new ModelAndView(model, "trimouhello.html");
    }).template();
    strudel.start();
  }

  static class TemplateModule extends AbstractModule {
    @Override
    protected void configure() {
      bind(TemplateEngine.class).to(TrimouEngine.class);
    }
  }
}
trimouhello.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ title }}</title>
</head>
<body>
<h1>
  {{ hello }}
</h1>
<p>
  {{ i18n "locaizationKey" }}
</p>
<a href="?lang=de">Click for switch to german</a>
</body>
</html>
Tip
See example DefaultTemplateEngine

Defaults of the template engines (classpath, locale)

The default classpath location for all templates is:
WEB-INF/template

However if you want to change it you can create the template module with a different classpath prefix (here: /de/ks/public/template):

Strudel strudel = Strudel.create(new MustacheModule("/de/ks/public/template"));

For those template engines supporting i18n I pass in the locale.

All template engines are @Singleton / soft singletons that are global for your injector.

Metrics

Strudel provides a basic interface for metrics the MetricsCallback.

With this interface you can collect basic statistics about your application to identify slow handlers, exceptions and unknown routes. This can be implemented by your own metrics collector or you can use one of the existing implementations:

  1. Dropwizard metrics

    compile "de.ks.strudel:strudel-metrics-dropwizard:$strudelversion"
  2. Avaje metrics

    compile "de.ks.strudel:strudel-metrics-avaje:$strudelversion"
Warning
Avaje metrics is a static-singleton library and I use manually created instances. In short this means that the standard reporters will not work. Stick to Dropwizard. That’s the cool stuff anyway.

I strongly recommend using the dropwizard implementation.

Tip
See example MetricsExample

Exception history

Both metric implementations implement an ExceptionHistory that does the following:

  1. store last 100 exceptions

  2. count duplicates (same stacktrace, class and message, overwriting doesn’t work)

  3. store first occurance and last occurance

Tip
Although quite known, your JVM should be started with -XX:-OmitStackTraceInFastThrow. Otherwise stack traces of reoccuring exceptions will be cut away

About

An undertow based java micro framework

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages