服务器作为Kotlin的功能– http4k

服务器作为Kotlin的功能- http4k

您是否听说过 "Server as a Function"?想法是,我们仅基于普通功能编写服务器应用程序,而该功能基于本文概述的概念 Your 由Twitter撰写和发布/马里斯·埃里克森(Marius Eriksen)。在Kotlin世界中,此概念最突出的实现是 http4k,维护者将其描述为"用Kotlin编写的HTTP工具集,重点是创建简单的,可测试的API"。最好的部分是http4k应用程序只是我们可以直接测试的Kotlin函数。看一下第一个示例:

第一个http4k服务器示例

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

This code shows a fully 功能性 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 的type HttpHandler 这里, which represents one of 的two essential concepts in 的idea of "Server as a Function":

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

Every http4k application can be composed of HttpHandlers in combination with 过滤s, both of which are simple type aliases for ordinary Kotlin function types. Http4k comes with 如果我们不将Kotlin标准库视为一个库的话,则依赖。由于纯格式的http4k应用程序仅需要嵌套的Kotlin函数;不涉及反射或注释处理。结果,http4k应用程序可以启动和停止 超级快 这也使它们成为部署在“功能即服务”环境中(而不是例如Spring Boot应用程序)的合理候选人。

更高级的http4k应用程序

让我们看一下http4k服务器的更高级示例。

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

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

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

val requestTimeLogger: 过滤 = 过滤 { 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 HttpHandlers, in which 的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 名称, and greets 的caller. In 的next step, we apply routing to 的handlers 通过 assigning one particular route to each handler. As we can see, 的pingPongHandler is used to serve a client who invokes /ping, while 的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 的original ones.

筛选器

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

fun 过滤.then(next: 过滤): 过滤

In 的example application above, we add our custom filter to one of 的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 过滤.then(next: HttpHandler): HttpHandler

As we can see, this again results in an HttpHandler. You all probably got 的idea 通过 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 的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 其他 separate HttpHandlers and 过滤s, and invoke it directly (e.g. in our tests). No HTTP 需要. 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 的unattractive pingPongHandler yields something similar to HTTP/1.1 200 OK ong! while calling 的final app handler gives us a gzipped response due to 的applied GZip filter. This call also implies a log informing about 的duration of 的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 的designated /ping URI when invoking 的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 的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 的stated concerns: 镜片.

基本定义

镜片,根据倍数 资源, were first used in 的哈斯克尔世界和are a 功能性 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 吸气剂 and a 二传手 focusing on precisely one part of Whole. A Part1Lens lens 吸气剂 would take an instance of Whole to return 的part it is focused on, i.e., part1. The lens 二传手, on 的other hand, takes a Whole along with a value to set 的focused part to and then returns a new Whole with 的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消息获取或在HTTP消息上设置特定值。用于描述镜头的相应API以DSL的形式出现,它也使我们能够定义要安装镜头的HTTP部分的要求(可选与强制)。由于HTTP消息是一个相当复杂的容器,因此我们可以将重点放在消息的不同区域:查询,标头,路径,FormField和正文。让我们看一些如何创建镜头的例子:

// lens focusing on 的path variable 名称
val 名称Lens = Path.string().of("名称")

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

// lens focusing on a 需要 and non empty string city 
val 非空Query = 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 的greet sample from earlier, let's modify our code to make use of 镜片 when retrieving a value:

val 名称Lens: 碧迪Lens<Request, String> =
    Query.nonEmptyString().required("名称")

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

We create a bidirectional lens focusing on 的query part of our message to extract a 需要 and non-empty 名称 from it. Now, if a client happens to call 的endpoint without providing a 名称 query parameter, 的lens automatically returns an error since it was defined as "required" and "nonEmpty". Please note that, 通过 default, 的application exposes much detail to 的client announcing 的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 的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 的名称Lens to set a value in an HTTP request?

val req = Request(GET, "/greet/{name}")
val reqWithName = 名称Lens.inject("科特林", req)
// alternatively, http4k offers a with function that can apply multiple 镜片 at once
val reqWithName = Request(GET, "/greet/{name}").with(
    名称Lens of "simon" //, more 镜片
)

The example shows how we create an instance of Request和inject a value via one or many 镜片. We can use 的Lens::inject function to specify 的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库。我个人更喜欢Jackson,因为它带有出色的Kotlin模块(对我的朋友表示敬意) 杰森·米纳德(Jayson Minard) 😉)。在将JSON格式模块添加到我们的应用程序之后,我们可以开始使用镜头将对象与HTTP消息进行封送处理。让我们考虑一个管理人员的部分完整的REST API:

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

class PersonHandlerProvider(private val service: PersonService) {
    private val personLens: 碧迪BodyLens<Person> = Body.auto<Person>().toLens()
    private val personListLens: 碧迪BodyLens<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 的client. We make use of a 碧迪BodyLens<List<Person>> (碧迪rectional) that we created via 的org.http4k.format.Jackson.auto extension for Jackson. As noted in 的http4k documentation, "auto()方法需要手动导入,因为IntelliJ不会自动选择它". We can use 的resulting lens like already shown earlier 通过 providing a value of type List<Person>和inject it into an HTTP Response as shown in 的getAllHandler implementation.
The postHandler, on 的other hand, provides an implementation of an HttpHandler, that extracts a Person entity from 的request and adds it to 的storage. Again, we use a lens to extract that JSON entity from 的request easily.

这已经结束了我们对镜片的偷窥。如我们所见,镜头是一个了不起的工具,它使我们能够提取和注入HTTP消息的各个部分,并提供验证这些部分的简单方法。现在,我们已经看到了http4k工具集的最基本概念,让我们考虑如何测试此类应用程序。

测验

Most of 的time, when we consider testing applications that sit on top of a web framework, we have to worry about details of that framework which can make testing 更难 than it should be. Spoiler Alert: This is not quite 的case with http4k 🎉

We have already learned that HttpHandlers, one of 的two core concepts in 的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, 的makers of http4k thought that it would still be helpful to provide some additional 模组s which support us with testing our applications. One of these 模组s is http4k-testing-hamkrest, which adds a set of Hamkrest 匹配器,我们可以用来更轻松地验证消息对象的详细信息。

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 的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 的resulting Response. 测验 过滤s, on 的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. 筛选器 map one HttpHandler into another one 通过 applying some intermediate pre or post-processing. Instead of investigating 的mapping between handlers itself, it would be more convenient to again send a Request through that filter and look into 的resulting Response. The good news is: It's super easy to do just that:

Http4k过滤器测试示例

val addExtraHeaderFilter = 过滤 { 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 的next filter. The goal is to send a simple request through that filter in our test. What we can do, is making 的filter a simple HttpHandler 通过 adding a dumb { Response(OK) } handler to it via then. As a result, we can invoke 的newly created handler, now containing our very own filter, and investigate whether 的resulting Response object contains 的new expected header. There we go - both handlers and filters got tested 🙃
总结一下,我想说的只是快速浏览一下使用大多数熟悉的工具测试http4k应用程序的愉快路径。可能有必要针对实际正在运行的服务器进行测试并在较低级别上验证响应,例如直接比较生成的JSON。这样做也是可能的,并通过 批准测试 模块。在本文的后面,我们想看看http4k的客户端模块,它再次打开了一些新的可能性。

无服务器

无服务器计算是当今最热门的主题之一。您知道,我们可以在其中运行代码的东西 其他 人们... 伺服器。其中一部分被称为 服务功能或FaaS,最常见的包括AWS Lambda,Google Cloud Functions和Microsoft Azure Functions。通常的想法是,这些供应商提供了一个平台,我们可以在其中部署代码,并且他们负责管理资源和按需扩展应用程序。 无服务器的缺点之一是,如果直到有人想再次使用它时才使用它,否则平台可能会降低我们的功能,这将需要重新启动。这对我们意味着什么?我们需要选择目标平台和工具,以快速启动我们的应用程序。例如,以经典形式出现在JVM上的Spring可能不是该用例的最佳工具。但是,正如您所能看到的那样,http4k占用空间小且启动时间超快是一个不错的选择。它甚至附带了对 AWS Lambda.

作为本文的一部分,我不会对此主题进行更深入的研究,但是我打算写一篇更详细的文章,介绍我们可以在FaaS平台上使用http4k做什么。敬请关注。

客户端功能

到现在为止,我们已经了解了http4k多么酷,以及为什么它是开发服务器应用程序的好工具。没有客户端使用HTTP Server并没有多大意义,因此我们希望通过另一面来总结“客户端即功能”。
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 的default implementation that comes with 的core library. If we were to prefer OkHttp, Apache, or Jetty instead, we would find a related 模组 to replace 的default. Since we program against interfaces (clients are HttpHandlers), 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 的ClientFilters.kt file that contains stuff like BasicAuth, Gzip和more stuff you'd expect for a client. The fact that all concepts of http4k 伺服器, including handlers, filters and also 镜片, can be reused in http4k clients opens up quite a few possibilities so that it can make much sense to e.g., use 的client 模组 to test your 伺服器 an vice versa.

摘要和监视

在过去的几周中,我个人学会了非常欣赏http4k。一旦您熟悉了基本概念,便可以快速地快速开发(包括测试)服务器应用程序。 Http4k附带了令人难以置信的受支持概念和技术列表,其中包括OAuth,Swagger,Websockets,XML等。它的模块化特性使我们能够通过根据需要应用依赖项来添加功能,并且由于其简单的基本类型,它具有很高的可扩展性。 Http4k是一个工具集,它使我们能够以快速的启动时间来编写应用程序,这也使它成为FaaS和无服务器计算的有效选择。似乎还不够,该工具集还包含用于编写HTTP客户端的复杂方法,这是我们在上一节中了解到的。总体而言,http4k是一项很有前途的技术,您在选择下一个HTTP工具集时一定要考虑。

如果您想进一步了解Kotlin及其出色的语言功能,请看我的演讲 "深入了解语言功能".

关于4个想法“服务器作为Kotlin的功能– http4k

发表评论

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