📜 ⬆️ ⬇️

Play! Lift! Srsly?

Play! and Lift, - these two frameworks are the personification of where the main flow of Scala web developers is heading. Truly, try searching for Scala frameworks on Stack Overflow and you will understand that I'm right. I believe that the percentage of sane people who are tired of complex harvesters is great, so I’ll tell you about the “other” Xitrum framework.

Xitrum is completely opposite to them in philosophy; it is a minimalist framework, the goal of which is to directly render content. There is no magic and no programming by convention. With its minimalism, it is close to Scalatra, but unlike it is completely asynchronous, because built on the basis of Netty (v4) and Akka (for more than a year now I have been following Scalatra and so far Netty support has not been announced). But do not worry, the threshold of entry is extremely low - the actors are only optional, although they are a significant plus in favor of the framework.

Immediately on performance. In the minimum configuration, xitrum runs and runs with a memory limit of 64Mb. Spending on CPU time is not significant, i.e. The framework itself does not load the processor. Everything else is up to you.

User Reviews
From the official site:
This is a really impressive body of work, arguably the most complete one.

Xitrum is truly a full stack web framework, including all the bases that are covered, and identifies a list of files. Tack on built-in JSON converter, before / around / after interceptors, request / session / cookie / flash scopes, integrated validation (for server & client-side, nice) , Netty (with Nginx, hello blazing fast), etc. and you have, wow.

My opinion:
The best framework I've ever seen for Scala / Java. Xitrum really catches me, it's like a mixture of Dancer + Rails with static typing, amazing!


')
The project is originally from Japan and has extensive documentation. The project developers leave a very pleasant impression, always listen to what they write in the official group and very quickly close bugs. For almost a year now, as I have not received a single failure or hung task in the tracker.

Ngoc Dao about your project (from the correspondence)
I began to develop Xitrum in the summer of 2010, for use in real-life Mobilus projects. At that time, Play supported only Java, and Lift was the only full-fledged framework for Scala. We tried to use it for several months, but it turned out that it is not so simple, at least for us who are familiar with Rails development. Therefore, as a technical manager, I decided to create a fast and scalable Scala web framework for my team, as easy to use as Rails. In fact, the result was more like Merb, rather than Rails (there is no data access layer in xitrum).


Over time, many people have participated in the development of the framework. Currently, the Xitrum core development team consists of two people: Oshida and Ngoc .


So, xitrum:


Xitrum is a controller-first framework. It is very easy to dynamically change the controller's views at runtime, which is not trivial for some Scala / Java frameworks. In my memory, this is generally the only framework from the Java world that allowed, without any crutches, to write a CMS with dynamic templating, so:

I think it would be appropriate to introduce the reader to the basics of the framework, I will describe more complex things another time if there is interest.

Creating an empty project and folder structure
The new project is easiest to create as follows:
git clone https://github.com/ngocdaothanh/xitrum-new my-app cd my-app sbt/sbt run 

By default, the server will start on port 8000. In the project, the Scalate template engine is connected by default. This is an ideal project to start, there is nothing superfluous in it, except for the standard controller and a pair of views that can be deleted.

To import a project into eclipse, use sbt / sbt eclipse , in idea sbt / sbt gen-idea .
Important: in eclipse, you need to manually add the config folder to the classpath, otherwise the project will not run from eclipse ( sbt-eclipse # 182 bug).

Project directory structure:
 ./script #      production ./config #   (akka, logback, xitrum) ./public #    (css, js, ) ./project # sbt ./src # src ./src/main/scalate #    ./src/main/scala # scala  ./src/main/scala/quickstart/Boot.scala #     



Simple controller


In xitrum, each request can only be processed by the heir from Action. That is, for each independent route processed by our server, we must declare a separate controller class.

 import xitrum.Action import xitrum.annotation.GET @GET("url/to/HelloAction") class HelloAction extends Action { def execute() { respondHtml( <xml:group> <p>Hello world!</p> </xml:group> ) } } 

Each new request arriving at the server will be processed by a new instance of the class, i.e., it does not make sense to store the state in these classes. It is very important to understand the fact that request processing is asynchronous. Unless you call the respond * () method, the connection with the client will not be closed and the client will wait for your response, perhaps forever. The execute method runs on a Netty stream, so you should not put long operations in it, for example:

 @GET("url/to/HelloAction") class HelloAction extends Action { def execute() { Thread.sleep(1000) // :    Netty  respond() } } 

With this implementation of the controller, your server is unlikely to serve more than 1 connection per second. To solve this problem, you need to use either FutureAction or ActorAction.


Routing



Xitrum supports all types of HTTP requests using GET, POST and other annotations. Any controller can handle an unlimited number of routes. You can determine the order of the controllers using the First and Last annotations. The default controller is defined as METHOD (": *")

 @GET("url1") @First class A extends Action { ... } @GET("url1", "url2", "...") @POST("url1", ...) class B extends Action { ... } @GET(":*") @Last class Default extends Action { ... } 

To get a link to the controller, the Action provides a url method that generates a GET link with parameters.

 url[HelloAction]("name" -> "caiiiycuk") // url/to/HelloAction?name=caiiiycuk 

A link to static resources from the public or classpath directory can be obtained using the publicUrl and resourceUrl methods, respectively. Classic redirects like forwardTo and redirectTo are supported .

Parsing parameters



Xitrum allows you to transparently work with three types of parameters:


Access to the parameters is very simple:

 param("X") //   X  String,      params("X") //   X  List[String],      paramo("X") //   X  Option[String] paramso("X") //   X  Option[List[String]] param[Type]("X") //   X  [Type],      params[Type]("X") //   X  List[[Type]],      paramo[Type]("X") //   X  Option[[Type]] paramso[Type]("X") //   X  Option[List[[Type]]] 

pathParams are set by analogy with Rails using the ':' symbol (:: id,: article,: etc), optionally, parameter values ​​can be restricted using regular expressions enclosed in '<>' (for example: id <[0-9] + >).

 @GET("articles/:id<[0-9]+>", "articles/:id<[0-9]+>.:format") class ArticlesShow extends Action { def execute() { val id = param[Int]("id") val format = paramo("format").getOrElse("json") ... } } 

Sometimes it becomes necessary to read the binary data of the POST request body; this is done like this:

 val body = requestContentString //  String val bodyMap = requestContentJson[Type] //  Json,  Type val raw = request.getContent //  ByteBuf 


Templates



By itself, xitrum does not have a built-in template engine; without a template engine, it is possible to generate the following types of response:


Chunked response support
There is a situation where the response to the request does not fit in the server’s memory. For example, our server generates an annual report in CSV format. Naturally in this situation, we cannot save the entire report in memory and send it to the client in one answer. Life cycle chunked response:
  1. Call the setChunked Method
  2. Call respond * () as many times as necessary
  3. Call respondLastChunk when all data is sent.

 val generator = new MyCsvGenerator setChunked() respondText(header, "text/csv") while (generator.hasNextLine) { val line = generator.nextLine respondText(line) } respondLastChunk() 


When using chunked response with ActorAction, you can very easily implement Facebook BigPipe .


For templating you can use Scalate , it is connected in a template project. The template engine supports several different syntaxes: mustache, scaml, jade and ssp. I prefer to use ssp because it is closest to html. In the template project, jade is configured, to change the type of syntax, in the xitrum.conf configuration, replace the line defaultType = jade with defaultType = ssp.

Scalate features
  • HTML compatible syntax (ssp)
  • HAML similar syntax (jade)
  • Download templates on the fly (runtime)
  • Compiled templates (error checking at compile time)
  • Inclusion of a template in a template
  • Pattern inheritance (the ability to override blocks)
  • Automatic tag escaping
  • Using Scala code directly in the template



When using Scalate for each controller, you can define your own view; according to the Scalate rules, the path to the template should correspond to the controller's package.

 src/main/scala/quickstart/action/SiteIndex.scala #   src/main/scalate/quickstart/action/SiteIndex.ssp #   src/main/scalate/quickstart/action/SiteIndex/ #    package quickstart.action import xitrum.annotation.GET @GET("") class SiteIndex extends DefaultLayout { def execute() { respondView() } } 

As you can see, to display the SiteIndex.ssp ​​template, it is enough to call respondView () . The concept of a fragment is provided, with it you can change the view of the controller.

 @GET("") class SiteIndex extends DefaultLayout { def execute() { respondHtml(renderFragment("some")) #      } } 


Xitrum does not impose restrictions on the strict correspondence of the view and the controller, so the same view can be used in different controllers. As a result, by default, templates can only use methods from the base action trait. You can transfer data to a template using the at method.

ControllerTemplate
 def execute() { at("login") = "caiiiycuk" at("rating") = 5 respondView() } 

 Hello ${at("login")} You rating is ${at("rating")} 


If you decide to limit yourself to one template per controller, then the pattern that allows you to import the current controller into the template will be very useful. When used, controller methods can be transparently invoked from a template.

ControllerTemplate
 def random = Random.nextInt def execute() { respondView() } 

 <% val myAction = currentAction.asInstanceOf[MyAction]; import myAction._ %> You random number is ${random} 


Of some interest is the atJson method, - it performs automatic conversion of models to Json, this turns out to be very useful when passing data directly to JavaScript.

ControllerTemplate
 case class User(login: String, name: String) ... def execute() { at("user") = User("admin", "Admin") respondView() } 

 <script type="text/javascript"> var user = ${atJson("user")}; alert(user.login); alert(user.name); </script> 



Session and cookies



Inside the controller, you need to use the requestCookies variable to access cookies, and to set a new cookie, respectively, responseCookies .

 //  requestCookies.get("myCookie") match { case None => ... case Some(string) => ... } //  responseCookies.append(new DefaultCookie("name", "value")) 

Xitrum automatically provides for saving, restoring and encrypting session in cookies. Work with session is carried out through the session variable.

 session.clear //   session("userId") = 1 //   session.isDefinedAt("userId") //   session("userId") //    


Filters



The request processing can be additionally controlled using filters; there are three of them: beforeFilter , afterFilter and aroundFilter . beforeFilter is executed before any processing of the request, if it returns false, then no further processing of the request by this controller will be performed. Opposite afterFilter are executed last.

 before1 -true-> before2 -true-> +--------------------+ --> after1 --> after2 | around1 (1 of 2) | | around2 (1 of 2) | | action | | around2 (2 of 2) | | around1 (2 of 2) | +--------------------+ 

Example, defining an internationalization language before processing a request.

 beforeFilter { val lango: Option[String] = yourMethodToGetUserPreferenceLanguageInSession() lango match { case None => autosetLanguage("ru", "en") case Some(lang) => setLanguage(lang) } true } def execute() { ... } 


Caching



So, request processing is simplified as follows: (1) request -> (2) before filters -> (3) execute controller method -> (4) after filters -> (5) response. Xitrum has built-in capabilities for caching the entire request processing chain (2 - 3 - 4 - 5) using the CachePageMinute annotation and the execute (3) method itself, the CacheActionMinute annotation. The cache lifetime is indicated in minutes. Only responses with a status of 200 Ok are included in the cache.

 import xitrum.Action import xitrum.annotation.{GET, CacheActionMinute, CachePageMinute} @GET("articles") @CachePageMinute(1) class ArticlesIndex extends Action { def execute() { ... } } @GET("articles/:id") @CacheActionMinute(10) class ArticlesShow extends Action { def execute() { ... } } 

By default, the framework uses its Lru cache implementation for caching. However, the implementation of the caching mechanism can be easily changed in the configuration. For a clustered cache, Hazelcast is the most suitable connection method .

In addition to annotations, xitrum provides access to the Cache object. It can be used to cache your data.

 import xitrum.Config.xitrum.cache // Cache with a prefix val prefix = "articles/" + article.id cache.put(prefix + "/likes", likes) cache.put(prefix + "/comments", comments) // Later, when something happens and you want to remove all cache related to the article cache.remove(prefix) 


Methods provided by the Cache object


RESTful API



Thanks to clear routing, implementing a RESTful API is trivial. Out-of-the-box Documentation API supported by Swagger

 import xitrum.{Action, SkipCsrfCheck} import xitrum.annotation.{GET, Swagger} @Swagger( Swagger.Note("Dimensions should not be bigger than 2000 x 2000") Swagger.OptStringQuery("text", "Text to render on the image, default: Placeholder"), Swagger.Response(200, "PNG image"), Swagger.Response(400, "Width or height is invalid or too big") ) trait ImageApi extends Action with SkipCsrfCheck { lazy val text = paramo("text").getOrElse("Placeholder") } @GET("image/:width/:height") @Swagger( // <--   ImageApi Swagger.Summary("Generate rectangle image"), Swagger.IntPath("width"), Swagger.IntPath("height") ) class RectImageApi extends Api { def execute { val width = param[Int]("width") val height = param[Int]("height") // ... } } @GET("image/:width") @Swagger( // <--   ImageApi Swagger.Summary("Generate square image"), Swagger.IntPath("width") ) class SquareImageApi extends Api { def execute { val width = param[Int]("width") // ... } } 

At runtime, xitrum will generate swagger.json which can be used in Swagger UI for easy viewing of documentation.

Important : for all POST requests, protection against CSRF attacks is provided, therefore, you should send csrf-token with any POST request, or, explicitly disable this protection using inheritance from the SkipCsrfCheck trait. Learn more about using csrf-token .

Internationalization
Internationalization is done using GNU gettext. The controller has a method t for performing internationalization.

 def execute() { respondHtml(t("hello_world")) } 

The current translation language is selected using the setLanguage method; in addition, the autosetLanguage method can be used to automatically select the language in accordance with the browser Accept-Language. To get the pot template, you need to run sbt compile. Files with translations should be put in the project classpath (usually in config / i18n). If the translation file was changed while the server was running, it will be re-read and the translation will be applied without restarting the server.


If you read this article to the end, then I suppose now you know about 70% of the framework functionality. And it seems to me that this confirms my idea that the threshold of entry is very low. Therefore, I recommend trying and asking questions.

The rest of the material you can always look at the official website in the textbook. Questions that I would like to additionally tell:


Sources


Demonstration project
Demonstration project showing most of what xitrum is capable of (it seems it is not very useful for training):
 git clone https://github.com/ngocdaothanh/xitrum-demos.git cd xitrum-demos sbt/sbt run 

Source: https://habr.com/ru/post/214913/


All Articles