Javanese Online

Эндпоинт — это функция

Самая популярная клиентская реализация RPC over HTTP — Retrofit. Он предлагает выразить интерфейс веб-сервера в виде interface, размеченного аннотациями.

Серверные реализации — Spring MVC, Play Framework и т. п. — выставляют размеченные аннотациями методы контроллера как эндпоинты.

Так, интерфейс или класс напоминает паттерны Service или Controller, то есть набор разрозненных функций, который притворяется объектом. Интерфейс для ретрофита обычно имеет единственную реализацию — сгенерированный на рантайме прокси-класс, а контроллер на сервере зачастую не реализовывает интерфейсов. ООП сюда вписывается плохо: AspectJ, Spring AOP, Spring Security пытаются изобразить декораторы, но получается сложно и неявно.

Я не знаю подходящей ООПшной абстракции, поэтому прибегну к функциям. В контексте Java и Kotlin функция — реализация интерфейса с единственным абстрактным методом (SAM-интерфейса). Функция — идеальный строительный блок: её можно декорировать, каррировать и частично применять, пайплайнить с другой функцией.

Задача: имея OkHttpClient и описание эндпоинта получать функцию, которая будет олицетворять один запрос; имея HTTP-сервер, роутер и описание эндпоинта — регистрировать функцию в качестве обработчика.

У первой части есть наивное решение: retrofit.create(SomeClass::class.java)::someMethod. У второй, в принципе, тоже: @Get("/route/{param}/) fun route(userName: String): User = someFunc(param). А теперь — более явно и без аннотаций, средствами модуля :http библиотеки Lychee:

// common
val user = GET("/user/{username}",
    Header("X-Token"), Path("username"),
    Response<Struct<User>>())

// client
val getUser: (token: String, username: String) -> CompletableFuture<Struct<Place>> =
    okHttpClient.template(baseUrl, user, completable {
        body!!.byteStream().reader().json().tokens().readAs(User)
    })

// server
val respond: HttpServerExchange.(Struct<User>) -> Unit = { user ->
    responseHeaders.add(contentType, "application/json; charset=utf-8")
    responseSender.send(TODO("$user to JSON"))
}
val respondBad: HttpServerExchange.(Param<*>, Throwable) -> Unit = { param, e ->
    val trace = String(ByteArrayOutputStream().also {
        e.printStackTrace(PrintStream(it))
    }.toByteArray())
    statusCode = 400
    responseHeaders.add(contentType, "text/html")
    responseSender.send("<pre>$trace</pre>") // debug only
}
Undertow.builder()
    .addHttpListener(
        port, host,
        RoutingHandler(false)
            .add(user, respond, respondBad) { token, username ->
                TODO("check token, fetch user")
            }
    )
    .build().start()

От сервера требуется уметь отвечать «400 Bad Request», потому и больше кода.

Теперь к плюсам функций.

Каррирование:

// val getUser: (token: String, username: String) -> CompletableFuture<Struct<Place>>
val getAuthorizedUser: (username: String) -> CompletableFuture<Struct<Place>> =
    getUser(token)

// curry
operator fun <T, U, R> ((T, U) -> R).invoke(t: T): (U) -> R =
    { u -> this(t, u) }

Декорирование:

RoutingHandler(false)
    .add(user, respond, respondBad, secured { TODO("fetch user") })

fun <T, R> secured(func: (T) -> R): HttpServerExchange.(token: String, T) -> R =
    { token, t ->
        TODO("check token, respond 401 if incorrect")
        func(t)
    }

Делегирование:

val userByUsername: (username: String) -> Struct<User> =
    session.query(
        "SELECT * FROM \"users\" WHERE \"username\" = ?", string,
        struct(Users, BindBy.Name))

Undertow.builder()
    .addHttpListener(
        port, host,
        RoutingHandler(false)
            .add(user, respond, respondBad, secured(userByUsername))
    )
    .build().start()

Минусы: имена параметров имеют свойство теряться; функции разной арности требуют разных обёрток.

Проект-пример, где есть и сервер, и клиент, которые используют одно объявление эндпоинтов.

Комментарии к статье

{"type":"articleComments","id":"567771c9-6348-4e45-9fad-9eb4d624a948","comments":[]}

Javanese.Online в GitHub

Чаты и каналы в Telegram

RSS-лента