服务器作为kotlin的函数– http4k

服务器作为kotlin的函数- http4k

你有没有听说过这个概念"Server as a Function"?这个想法是我们基于普通函数来编写我们的服务器应用程序,该函数基于纸张中概述的概念 您的服务器作为函数 由Twitter编写和发布/Marius Eriksen.。在Kotlin World中,这个概念最突出的实现是 http4k,维护者描述为彩票3d字谜"用kotlin编写的http工具集,专注于创建简单,可测试的apis"。关于它的最佳部分是HTTP4K应用程序只是我们可以直截了当地测试的Kotlin功能。看看这个例子:

First HTTP4K服务器示例

val app: HttpHandler = { request: Request -> Response(OK).body(request.body) }
val server = app.asServer(SunHttp(8000)).start()

This code shows a fully functional http4k application consisting of a single Kotlin function app which we embedded into a SunHttp server, one example of available server implementations we may choose from. Note the type httphandler. here, which represents one of the two essential concepts in the idea of "Server as a Function":

  • httphandler. ((Request) -> Response): abstraction to process HTTP requests into responses by mapping the first into the latter
  • 筛选 (httphandler. -> HttpHandler): abstraction to add pre and post-processing like caching, debugging, authentication handling, and more to an httphandler.. Filters are composable/stackable

Every http4k application can be composed of httphandler.s in combination with 筛选s, both of which are simple type aliases for ordinary Kotlin function types. Http4k comes with 依赖关系如果我们没有将kotlin标准库计数为彩票3d字谜。自HTTP4K应用程序以其纯粹的形式,只需要一些嵌套的Kotlin功能;没有涉及反射或注释处理。结果,HTTP4K应用程序可以启动和停止 超级 这也使它们能够在函数 - AS-Service环境上部署合理的候选者(而不是例如Spring Boot应用程序)。

更高级的HTTP4K应用程序

让我们来看看彩票3d字谜更高级的HTTP4K服务器的示例。

val pingPongHandler: HttpHandler = { _ -> Response(OK).body("乒!") }

val greetHandler: HttpHandler = { req: Request ->
    val name: String? = req.query("姓名")
    Response(OK).body("hello ${name ?: "unknown!"}")
}

val routing: RoutingHttpHandler = routes(
     "/ping" bind GET to pingPongHandler,
     "/greet" bind GET to greetHandler
)

val requestTimeLogger: Filter = Filter { next: HttpHandler ->
     { request: Request ->
         val start = clock.millis()
         val response = next(request)
         val latency = clock.millis() - start
         logger { "Request to ${request.uri} took ${latency}ms" }
         response
     }
}

val app: HttpHandler =
     ResponseFilters.GZip()
     .then(requestTimeLogger)
     .then(routing)

In this snippet, we can see a few exciting things http4k applications may entail. The first two expressions are definitions of httphandler.s, in which the first one takes any response and maps it to an OK response containing "pong" in its body. The second handler takes a request, extracts a name, and greets the caller. In the next step, we apply routing to the handlers by assigning one particular route to each handler. As we can see, the pingPongHandler is used to serve a client who invokes /ping, while the greetHandler is used to cover /greet.

路由

路由 in http4k works with arbitrary levels of nesting, which works flawlessly since routing itself results in a new httphandler. (strictly speaking, a special kind of type 路由HttpHandler), just like the original ones.

过滤器

As mentioned before, the other important concept we want to look at is 筛选s. For starters, we create a requestTimeLogger that intercepts each incoming request by measuring its processing time and logging the elapsed time. Filters can be combined using the then method, which allows us to define chains of filters. The corresponding API looks like this:

fun Filter.then(next: Filter): Filter

In the example application above, we add our custom filter to one of the default filters called GZip. Once we have combined all our filters, we want to add an httphandler. to our filter chain. Again, there's a then function we can use to do so:

fun Filter.then(next: HttpHandler): HttpHandler

As we can see, this again results in an httphandler.. You all probably got the idea by now - It only needs two simple types to express how an HTTP server should operate.

The shown GZip filter is just one of many default filters we may choose from. Others cover concerns like caching, CORS, basic authentication or cookie handling and can be found in the org.http4k.filter package.

调用httphandlers.

So what did we get out of that nesting which resulted in yet another httphandler. called app? Well, this itself does not entail running an actual server yet. However, it describes how requests are handled. We can use this object, as well as other separate httphandler.s and 筛选s, and invoke it directly (e.g. in our tests). No HTTP required. Let's see this in action:

//call handler directly
val handlerResponse: Response = pingPongHandler(Request(GET, "/any"))

//call handler through routing
val routingCallResponse: Response = app(Request(GET, "/ping").header("accept-encoding", "gzip"))

app.asServer(Jetty(9000)).start()

Http4k comes with its own implementations of a Request和a Response, first of which can be used to invoke an httphandler.. Calling the unattractive pingPongHandler yields something similar to HTTP/1.1 200 OK pong! while calling the final app handler gives us a gzipped response due to the applied GZip filter. This call also implies a log informing about the duration of the request: 2019-09-20T21:22:55.300768Z LOG - Request to /ping took 3ms. Please note that, while it was fine to call pingPongHandler with a random URI (/any), we had to use the designated /ping URI when invoking the routing-backed app.
Last, but not least, we start our very own http4k httphandler. as a server on a Jetty with port 9000. Find a list of available server implementations 这里.

镜头

One of the things a sophisticated HTTP app has to deal with is taking stuff out and also putting stuff into HTTP messages. When we take parameters out of requests, we also care about validating these values. Http4k comes with a fascinating concept that helps us deal with the stated concerns: 镜头.

基本定义

镜片,根据多个 资源,首先使用了 哈斯克尔世界和are a functional concept that may appear slightly hard to understand. Let me try to describe it in a shallow, understandable manner. Let's say we have a class Whole which comes with different fields part1, part2, and so on. A lens basically composes a getter and a setter focusing on precisely one part of Whole. A Part1Lens lens 吸手 would take an instance of Whole to return the part it is focused on, i.e., part1. The lens 骗局, on the other hand, takes a Whole along with a value to set the focused part to and then returns a new Whole with the updated part. Remember that a lens can be used to both get and set a part of a whole object. Now, let's learn how this concept helps us with handling HTTP messages.

镜头在http4k.

在镜头的基本思想之后,HTTP4K镜头是双向实体,其可用于从/到HTTP消息中获取或设置特定值。描述镜头的相应API以DSL的形式出现,这也让我们定义了我们安装镜头的HTTP部件的HTTP部件的要求(可选与vs。由于HTTP消息是彩票3d字谜相当复杂的容器,因此我们可以在消息的不同区域上具有镜头:查询,标题,路径,Formfield,Body。让我们看看如何创建镜头的一些示例:

// lens focusing on the path variable name
val nameLens = Path.string().of("姓名")

// lens focusing on a required query parameter city
val requiredQuery = Query.required("city")

// lens focusing on a required and non empty string city 
val nonEmptyQuery = Query.nonEmptyString().required("city")

// lens focusing on an optional header Content-Length with type int 
val optionalHeader = Header.int().optional("Content-Length")

// lens focusing on text body
val responseBody = Body.string(ContentType.TEXT_PLAIN).toLens()

到目前为止,用于创建镜头的API或多或少直截了当,但在目标上使用它们呢?这是伪代码语法
a) Retrieving a value: <lens>.extract(<target>), or <lens>(<target>)
b) Setting a value: <lens>.inject(<value>, <target>), or <lens>(<value>, <target>)

使用镜头从HTTP请求中检索值

Reusing the greet sample from earlier, let's modify our code to make use of lenses when retrieving a value:

val nameLens: BiDiLens<Request, String> =
    Query.nonEmptyString().required("姓名")

val greetHandler: HttpHandler = { req: Request ->
     val name: String = nameLens.extract(req) //or nameLens(req)
     Response(OK).body("hello $name")
}

We create a bidirectional lens focusing on the query part of our message to extract a required and non-empty name from it. Now, if a client happens to call the endpoint without providing a 姓名 query parameter, the lens automatically returns an error since it was defined as "required" and "nonEmpty". Please note that, by default, the application exposes much detail to the client announcing the error as org.http4k.lens.LensFailure: query 'name' must be string including a detailed stack trace. Rather than that, we want to map all lens errors to HTTP 400 responses which implies that the client provided invalid data. Therefore, http4k offers a ServerFilters.CatchLensFailure filter which we can easily activate in our filter chain:

// gzip omitted
val app: HttpHandler = ServerFilters.CatchLensFailure
 .then(requestTimeLogger)
 .then(routing)

使用镜头在HTTP请求中设置值

After looking into extracting values from HTTP messages, how can we use the 姓名Lens to set a value in an HTTP request?

val req = Request(GET, "/greet/{name}")
val reqWithName = nameLens.inject("kotlin", req)
// alternatively, http4k offers a with function that can apply multiple lenses at once
val reqWithName = Request(GET, "/greet/{name}").with(
    nameLens of "simon" //, more lenses
)

The example shows how we create an instance of Request和inject a value via one or many lenses. We can use the Lens::inject function to specify the value we want to set into an arbitrary instance of Request. Now that we saw a basic example of a string lens, we want to dig into handling some more advanced JSON content.

json处理

我们可以选择几个JSON实现,包括例如普通的GSON和Jackson库。我个人更喜欢杰克逊,因为它附带了彩票3d字谜伟大的kotlin模块(Kedos给我的朋友 Jayson Minard. 😉)。在向我们的应用程序添加JSON格式模块后,我们可以使用镜头开始将对象从HTTP消息开始。让我们考虑彩票3d字谜管理人员的部分完整的休息API:

[...]
import org.http4k.format.Jackson.auto

class PersonHandlerProvider(private val service: PersonService) {
    private val personLens: BiDiBodyLens<Person> = Body.auto<Person>().toLens()
    private val personListLens: BiDiBodyLens<List<Person>> = Body.auto<List<Person>>().toLens()

     fun getAllHandler(): HttpHandler = {
             Response(OK).with( 
                 personListLens of service.findAll()
             )
        }

     fun postHandler(): HttpHandler = { req ->
             val personToAdd = personLens.extract(req)
             service.add(personToAdd)
             Response(OK)
         }

     //...more
}

In this example, we see a class that provides two handlers representing common actions you would expect from a REST API. The getAllHandler fetches all currently stored entities and returns them to the client. We make use of a Bidi.BodyLens<List<Person>> (Bidi.rectional) that we created via the org.http4k.format.Jackson.auto extension for Jackson. As noted in the http4k documentation, "Auto()方法需要手动导入,因为Intellij不会自动拾取它". We can use the resulting lens like already shown earlier by providing a value of type List<Person>和inject it into an HTTP Response as shown in the getAllHandler implementation.
The postHandler, on the other hand, provides an implementation of an httphandler., that extracts a Person entity from the request and adds it to the storage. Again, we use a lens to extract that JSON entity from the request easily.

这已经得出了潜在的镜头偷看。正如我们所看到的,镜头是彩票3d字谜奇妙的工具,可以让我们提取和注入HTTP消息的部分,并提供简单的验证这些部件的方法。现在,我们已经看到了http4k工具集的最基本的概念,让我们考虑我们如何测试这些应用程序。

测试

大多数时候,当我们考虑测试坐在Web框架之上的应用程序时,我们必须担心该框架的细节,这可能会使测试比它更难。 SPOILER ALERT:HTTP4K并不是如此🎉

We have already learned that httphandler.s, one of the two core concepts in the http4k toolset, are just regular Kotlin functions mapping requests to responses and even a complete http4k application again is just an httphandler.和thus a callable function. As a result, entire and partial http4k apps can be tested easily and without additional work. Nevertheless, the makers of http4k thought that it would still be helpful to provide some additional modules which support us with testing our applications. One of these modules is http4k-testing-hamkrest, which adds a set of 哈姆克里斯特 匹配器,我们可以使用更轻松地验证消息对象的详细信息。

http4k处理程序测试示例

import com.natpryce.hamkrest.assertion.assertThat
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Status
import org.http4k.hamkrest.hasStatus
import org.junit.jupiter.api.Test

class PersonHandlerProviderTest {

    val systemUnderTest = PersonHandlerProvider(PersonService())

    @Test
    fun getAll_handler_can_be_invoked_as_expected(){
        val getAll: HttpHandler = systemUnderTest.getAllHandler()

        val result: Response = getAll(Request(Method.GET, "/some-uri"))

        assertThat(result, hasStatus(Status.OK))
    }
}

This snippet demonstrates a test for the PersonHandlerProvider we have worked with earlier already. As shown, it's pretty straightforward to call an httphandler. with a Request object and then use Hamkrest or whatever assertion library you prefer to check the resulting Response. Testing 筛选s, on the other hand, is "harder". To be honest though, it's just one tiny thing we need to do on top of what we did with handlers. Filters map one httphandler. into another one by applying some intermediate pre or post-processing. Instead of investigating the mapping between handlers itself, it would be more convenient to again send a Request through that filter and look into the resulting Response. The good news is: It's super easy to do just that:

HTTP4K过滤器测试示例

val addExtraHeaderFilter = Filter { next ->
    {
        next(it).header("x-extra-header", "some value")
    }
}

@Test
fun adds_a_special_header() {
    val handler: HttpHandler = addExtraHeaderFilter.then { Response(OK) }

    val response: Response = handler(Request(GET, "/echo"))

    assertThat(response, hasStatus(OK).and(hasHeader("x-extra-header", "some value")))
}

We have a 筛选 called addExtraHeaderFilter that adds a custom header to a processed request and then forwards it to the next filter. The goal is to send a simple request through that filter in our test. What we can do, is making the filter a simple httphandler. 经过 adding a dumb { Response(OK) } handler to it via then. As a result, we can invoke the newly created handler, now containing our very own filter, and investigate whether the resulting Response object contains the new expected header. There we go - both handlers and filters got tested 🙃
要包装,我想说这只是彩票3d字谜快速看看拥有大多数熟悉的工具的HTTP4K应用程序的快乐路径。可能有必要对实际运行服务器进行测试,并验证对较低级别的响应,例如,直接比较生成的JSON。这样做也可以通过 批准测试 模块。在本文之后,我们想查看HTTP4K的客户端模块,再次开辟了一些新的可能性。

无服务器

我们该时间的最热门主题之一是无服务器计算。你知道,我们可以在哪里运行我们的代码 其他 人们... 服务器。它的一部分被称为 函数或者FAAS和最常见的FA包括AWS Lambda,Google云功能和Microsoft Azure函数。一般思想是,这些供应商提供了彩票3d字谜我们可以部署代码的平台,并且他们会根据需要处理资源并缩放我们的应用程序。无服务器的缺点是,如果未使用它,我们的功能可能会被平台旋转,直到有人想再次使用它,这将需要彩票3d字谜新的启动。对我们来说意味着什么?我们需要选择允许我们应用程序快速启动的目标平台和工具。例如,其经典形式的JVM上的春天可能不是该用例的最佳工具。但是,正如您可以图像的那样,HTTP4K占地面积小,超级快速启动时间是彩票3d字谜很好的选择。它甚至存在本地人支持 AWS lambda..

作为本文的一部分,我不会深入了解这一主题,但我计划在FAAS平台上使用HTTP4K编写更详细的帖子。敬请关注。

客户作为函数

到目前为止,我们已经学会了HTTP4K的酷和为什么是开发服务器应用程序的彩票3d字谜很好的工具。 HTTP服务器在没有客户端的情况下没有大量意义,因此我们希望通过以函数查看另一边 - 客户来完成本文。
The http4k core library comes with everything we need to get started with clients. Clients in http4k again are just a special form of an httphandler., as we can see in this little snippet:

val request = Request(Method.GET, "//airportparkinghotels.net/sitemap.xml")
val client: HttpHandler = JavaHttpClient()
val response = client(request)

The shown JavaHttpClient is the default implementation that comes with the core library. If we were to prefer OkHttp, Apache, or Jetty instead, we would find a related 模块 to replace the default. Since we program against interfaces (clients are httphandler.s), it's not a big deal to swap out implementations at any time. The core library obviously comes with several default 筛选s we can apply to our client which can be found in the ClientFilters.kt file that contains stuff like BasicAuth, Gzip和more stuff you'd expect for a client. The fact that all concepts of http4k servers, including handlers, filters and also lenses, can be reused in http4k clients opens up quite a few possibilities so that it can make much sense to e.g., use the client module to test your servers an vice versa.

摘要和监视

在过去的几周里,我个人学会了很多很多。一旦您对基本概念感到满意,它将快速开发(包括测试)服务器应用程序即将开始。 HTTP4K附带彩票3d字谜令人难以置信的支持概念和技术列表,包括OAuth,Swagger,WebSockets,XML和许多人更多。它的模块化性质允许我们通过根据需要应用依赖项来添加功能,并且由于其简单的基础类型,它是高度可扩展的。 HTTP4K是一种工具集,允许我们使用快速启动时间编写应用程序,这也使其成为FAAS和无服务器计算的有效替代方案。好像这不够,该工具集还包括用于编写HTTP客户端的复杂手段,我们在最后一节中学习了它。总的来说,HTTP4K是彩票3d字谜有希望的技术,您当然应该在选择下彩票3d字谜HTTP工具集时考虑。

如果您想了解有关Kotlin及其梦幻般的语言功能的更多信息,请看看我的谈话 "潜入高级语言功能".

4 thoughts on “服务器作为kotlin的函数– http4k

  • 斯蒂芬

    Hallo Simon,

    非常好的博客文章。 http4k似乎是彩票3d字谜非常有趣的网络框架。值得探索。
    我想知道是否在

    val greetHandler: HttpHandler = { req: Request ->
         val name: String? = nameLens.extract(req) //or nameLens(req)
         Response(OK).body("hello ${name ?: "unknown!"}")
    }
    

    这“name”应该/可能是彩票3d字谜不可空的类型。

    有关HTTP4K的任何想法或信息“answer” to couroutines?

    Tschüß,
    Stefan

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *