Javalin 快速开始

https://javalin.io/documentation#getting-started

添加依赖项:

io.javalin javalin 6.6.0

如果您想要将 Javalin 与测试工具 Jackson 和 Logback 一起使用,则可以使用 artifact id javalin-bundle 代替javalin。

开始编码:

import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(/*config*/)
            .get("/", ctx -> ctx.result("Hello World"))
            .start(7070);
    }
}

处理程序

Javalin 有三种主要的处理程序类型:前置处理程序端点处理程序后置处理程序。(此外还有异常处理程序和错误处理程序,但我们稍后会讲到它们)。前置处理程序、端点处理程序和后置处理程序都需要三个部分:

  • 动词,以下之一:before,,,,,,(…,,,,)get,post,put,patch,delete,after head,options,trace,connect
  • 路径,例如:/,,/hello-world/hello/{name}
  • 处理程序实现,例如ctx -> { … }、MyClass implements Handler等

该接口的返回类型为 void。您可以使用、 或 之Handler类的方法来设置将返回给用户的响应。ctx.result(result)ctx.json(obj)ctx.future(future)

如果您为同一路径添加多个 before/after 处理程序,它们将按照添加的顺序执行。这对于添加身份验证、缓存、日志记录等功能非常有用。

处理程序之前
每次请求(包括静态文件)之前都会匹配 Before-handler。

您可能知道其他库中的 before-handler 是过滤器、拦截器或中间件。
app.before(ctx -> {
// runs before all requests
});
app.before(“/path/*”, ctx -> {
// runs before request to /path/*
});
在某些情况下,你可能希望仅当请求被匹配(而不是 404)时才运行 before-handler。在这种情况下,你可以使用以下app.beforeMatched方法:

app.beforeMatched(ctx -> {
// runs before all matched requests (including static files)
});
端点处理程序
端点处理程序是主要的处理程序类型,它定义了您的 API。您可以添加 GET 处理程序来向客户端提供数据,或者添加 POST 处理程序来接收数据。常用方法直接在Javalin类上支持(GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS),不常用操作(TRACE、CONNECT)则通过 来支持Javalin#addHandler。

端点处理程序按照其定义的顺序进行匹配。

您可能知道端点处理程序是来自其他库的路由或中间件。
app.get(“/output”, ctx -> {
// some code
ctx.json(object);
});

app.post(“/input”, ctx -> {
// some code
ctx.status(201);
});
处理程序路径可以包含路径参数。这些可通过以下方式获取ctx.pathParam(“key”):

app.get(“/hello/{name}”, ctx -> { // the {} syntax does not allow slashes (‘/‘) as part of the parameter
ctx.result(“Hello: “ + ctx.pathParam(“name”));
});
app.get(“/hello/“, ctx -> { // the <> syntax allows slashes (‘/‘) as part of the parameter
ctx.result(“Hello: “ + ctx.pathParam(“name”));
});
处理程序路径还可以包含通配符参数:

app.get(“/path/*”, ctx -> { // will match anything starting with /path/
ctx.result(“You are here because “ + ctx.path() + “ matches “ + ctx.matchedPath());
});
但是,您无法提取通配符的值。如果需要此行为,请使用斜线 (而不是) 来接受路径参数。{param-name}

处理程序之后
每次请求后都会运行后续处理程序(即使发生异常)

您可能知道后处理程序是来自其他库的过滤器、拦截器或中间件。
app.after(ctx -> {
// run after all requests
});
app.after(“/path/*”, ctx -> {
// runs after request to /path/*
});
在某些情况下,你可能希望仅当请求被匹配(而不是 404)时才运行后续处理程序。在这种情况下,你可以使用以下app.afterMatched方法:

app.afterMatched(ctx -> {
// runs after all matched requests (including static files)
});
语境
该Context对象提供了处理 http 请求所需的一切。它包含底层的 servlet 请求和 servlet 响应,以及一系列 getter 和 setter 方法。

// Request methods
body() // request body as string
bodyAsBytes() // request body as array of bytes
bodyAsClass(clazz) // request body as specified class (deserialized from JSON)
bodyStreamAsClass(clazz) // request body as specified class (memory optimized version of above)
bodyValidator(clazz) // request body as validator typed as specified class
bodyInputStream() // the underyling input stream of the request
uploadedFile(“name”) // uploaded file by name
uploadedFiles(“name”) // all uploaded files by name
uploadedFiles() // all uploaded files as list
uploadedFileMap() // all uploaded files as a “names by files” map
formParam(“name”) // form parameter by name, as string
formParamAsClass(“name”, clazz) // form parameter by name, as validator typed as specified class
formParams(“name”) // list of form parameters by name
formParamMap() // map of all form parameters
pathParam(“name”) // path parameter by name as string
pathParamAsClass(“name”, clazz) // path parameter as validator typed as specified class
pathParamMap() // map of all path parameters
basicAuthCredentials() // basic auth credentials (or null if not set)
attribute(“name”, value) // set an attribute on the request
attribute(“name”) // get an attribute on the request
attributeOrCompute(“name”, ctx -> {}) // get an attribute or compute it based on the context if absent
attributeMap() // map of all attributes on the request
contentLength() // content length of the request body
contentType() // request content type
cookie(“name”) // request cookie by name
cookieMap() // map of all request cookies
header(“name”) // request header by name (can be used with Header.HEADERNAME)
headerAsClass(“name”, clazz) // request header by name, as validator typed as specified class
headerMap() // map of all request headers
host() // host as string
ip() // ip as string
isMultipart() // true if the request is multipart
isMultipartFormData() // true if the request is multipart/formdata
method() // request methods (GET, POST, etc)
path() // request path
port() // request port
protocol() // request protocol
queryParam(“name”) // query param by name as string
queryParamAsClass(“name”, clazz) // query param by name, as validator typed as specified class
queryParamsAsClass(“name”, clazz) // query param list by name, as validator typed as list of specified class
queryParams(“name”) // list of query parameters by name
queryParamMap() // map of all query parameters
queryString() // full query string
scheme() // request scheme
sessionAttribute(“name”, value) // set a session attribute
sessionAttribute(“name”) // get a session attribute
consumeSessionAttribute(“name”) // get a session attribute, and set value to null
cachedSessionAttribute(“name”, value) // set a session attribute, and cache the value as a request attribute
cachedSessionAttribute(“name”) // get a session attribute, and cache the value as a request attribute
cachedSessionAttributeOrCompute(…) // same as above, but compute and set if value is absent
sessionAttributeMap() // map of all session attributes
url() // request url
fullUrl() // request url + query string
contextPath() // request context path
userAgent() // request user agent
req() // get the underlying HttpServletRequest

// Response methods
result(“result”) // set result stream to specified string (overwrites any previously set result)
result(byteArray) // set result stream to specified byte array (overwrites any previously set result)
result(inputStream) // set result stream to specified input stream (overwrites any previously set result)
future(futureSupplier) // set the result to be a future, see async section (overwrites any previously set result)
writeSeekableStream(inputStream) // write content immediately as seekable stream (useful for audio and video)
result() // get current result stream as string (if possible), and reset result stream
resultInputStream() // get current result stream
contentType(“type”) // set the response content type
header(“name”, “value”) // set response header by name (can be used with Header.HEADERNAME)
redirect(“/path”, code) // redirect to the given path with the given status code
status(code) // set the response status code
status() // get the response status code
cookie(“name”, “value”, maxAge) // set response cookie by name, with value and max-age (optional).
cookie(cookie) // set cookie using javalin Cookie class
removeCookie(“name”, “/path”) // removes cookie by name and path (optional)
json(obj) // calls result(jsonString), and also sets content type to json
jsonStream(obj) // calls result(jsonStream), and also sets content type to json
html(“html”) // calls result(string), and also sets content type to html
render(“/template.tmpl”, model) // calls html(renderedTemplate)
res() // get the underlying HttpServletResponse

// Other methods
async(runnable) // lifts request out of Jetty’s ThreadPool, and moves it to Javalin’s AsyncThreadPool
async(asyncConfig, runnable) // same as above, but with additonal config
handlerType() // handler type of the current handler (BEFORE, AFTER, GET, etc)
appData(typedKey) // get data from the Javalin instance (see app data section below)
with(pluginClass) // get context plugin by class, see plugin section below
matchedPath() // get the path that was used to match this request (ex, “/hello/{name}”)
endpointHandlerPath() // get the path of the endpoint handler that was used to match this request
cookieStore() // see cookie store section below
skipRemainingHandlers() // skip all remaining handlers for this request
应用数据
应用数据可以通过 注册到 Javalin 实例上Javalin#create,然后通过appData(…)中的方法访问Context。您需要为您的数据创建一个类型化的键,然后将其注册到 Javalin 实例上。

// register a custom attribute
var myKey = new Key(“my-key”);
var app = Javalin.create(config -> {
config.appData(myKey, myValue);
});
// access a custom attribute
var myValue = ctx.appData(myKey); // var will be inferred to MyValue
// call a custom method on a custom attribute
ctx.appData(myKey).myMethod();
将密钥存储为静态字段会很有帮助,但您也可以在每次需要时重新创建密钥。

饼干店
该类CookieStore提供了一种在处理程序、请求甚至服务器之间共享信息的便捷方法:

ctx.cookieStore().set(key, value); // store any type of value
ctx.cookieStore().get(key); // read any type of value
ctx.cookieStore().clear(); // clear the cookie-store
cookieStore 的工作原理如下:

与传入请求匹配的第一个处理程序将使用当前存储在 cookie 中的数据(如果有)填充 cookie-store-map。
现在,该映射可以用作同一请求周期中处理程序之间的状态,其方式与ctx.attribute()
在请求周期结束时,cookie-store-map 会被序列化、base64 编码,并以 cookie 的形式写入响应中。这允许你在请求和服务器之间共享该 map(如果你在负载均衡器后运行多个服务器)。
例子:
serverOneApp.post(“/cookie-storer”, ctx -> {
ctx.cookieStore().set(“string”, “Hello world!”);
ctx.cookieStore().set(“i”, 42);
ctx.cookieStore().set(“list”, Arrays.asList(“One”, “Two”, “Three”));
});
serverTwoApp.get(“/cookie-reader”, ctx -> { // runs on a different server than serverOneApp
String string = ctx.cookieStore().get(“string”)
int i = ctx.cookieStore().get(“i”)
List list = ctx.cookieStore().get(“list”)
});
由于客户端存储了 cookie,因此get对 的请求将能够检索在对serverTwoApp 中传递的信息。postserverOneApp

请注意,cookie 的最大大小为 4kb。

WebSockets
Javalin 处理 WebSocket 的方式非常直观。你可以使用路径声明一个端点,并在 lambda 表达式中配置不同的事件处理程序:

app.ws(“/websocket/{path}”, ws -> {
ws.onConnect(ctx -> System.out.println(“Connected”));
});
总共支持五种事件:

ws.onConnect(WsConnectContext)
ws.onError(WsErrorContext)
ws.onClose(WsCloseContext)
ws.onMessage(WsMessageContext)
ws.onBinaryMessage(WsBinaryMessageContext)
不同的上下文会WsContext暴露不同的内容,例如, WsMessageContext会有一个方法.message()返回客户端发送的消息。不同上下文之间的差异很小,完整的概述可以在WsContext部分查看。

您可以在常见问题解答 - 并发中了解 Javalin 如何处理 WebSocket 并发。

WsBefore
这app.wsBefore会添加一个在 WebSocket 处理程序之前运行的处理程序。每个 WebSocket 端点可以拥有任意数量的前置处理程序,并且所有事件均受支持。

app.wsBefore(ws -> {
// runs before all WebSocket requests
});
app.wsBefore(“/path/*”, ws -> {
// runs before websocket requests to /path/*
});
WsEndpoint
WebSocket 端点用 声明app.ws(path, handler)。WebSocket 处理程序需要唯一的路径。

app.ws(“/websocket/{path}”, ws -> {
ws.onConnect(ctx -> System.out.println(“Connected”));
ws.onMessage(ctx -> {
User user = ctx.messageAsClass(User.class); // convert from json
ctx.send(user); // convert to json and send back
});
ws.onBinaryMessage(ctx -> System.out.println(“Message”))
ws.onClose(ctx -> System.out.println(“Closed”));
ws.onError(ctx -> System.out.println(“Errored”));
});

这app.wsAfter会添加一个在 WebSocket 处理程序之后运行的处理程序。每个 WebSocket 端点可以拥有任意数量的后续处理程序,并且所有事件均受支持。

app.wsAfter(ws -> {
// runs after all WebSocket requests
});
app.wsAfter(“/path/*”, ws -> {
// runs after websocket requests to /path/*
});
WsContext
该WsContext对象提供了处理 websocket 请求所需的一切。它包含底层的 websocket 会话和 servlet 请求,以及用于向客户端发送消息的便捷方法。

// Session methods
send(obj) // serialize object to json string and send it to client
send(“message”) // send string to client
send(byteBuffer) // send bytes to client
sendAsClass(obj, clazz) // serialize object to json string and send it to client

// Upgrade Context methods (getters)
matchedPath() // get the path that was used to match this request (ex, “/hello/{name}”)
host() // host as string

queryParam(“name”) // query param by name as string
queryParamAsClass(“name”, clazz) // query param parameter by name, as validator typed as specified class
queryParams(“name”) // list of query parameters by name
queryParamMap() // map of all query parameters
queryString() // full query string

pathParam(“name”) // path parameter by name as string
pathParamAsClass(“name”, clazz) // path parameter as validator typed as specified class
pathParamMap() // map of all path parameters

header(“name”) // request header by name (can be used with Header.HEADERNAME)
headerAsClass(“name”, clazz) // request header by name, as validator typed as specified class
headerMap() // map of all request headers

cookie(“name”) // request cookie by name
cookieMap() // map of all request cookies

attribute(“name”, value) // set an attribute on the request
attribute(“name”) // get an attribute on the request
attributeMap() // map of all attributes on the request

sessionAttribute(“name”) // get a session attribute
sessionAttributeMap() // map of all session attributes

sendPing() // send a ping to the client
sendPing(bytes) // send a ping with data to the client
enableAutomaticPings() // enable automatic pinging to avoid timeouts
enableAutomaticPings(1, HOURS, bytes) // enable automatic pinging with custom interval and/or data
disableAutomaticPings() // disable automatic pinging

closeSession() // close the session
closeSession(closeStatus) // close the session with a CloseStatus
closeSession(400, “reason”) // close the session with a status and reason
WsMessageContext
message() // receive a string message from the client
messageAsClass(clazz) // deserialize message from client
WsBinaryMessageContext
data() // receive a byte array of data from the client
offset() // the offset of the data
length() // the length of the data
WsCloseContext
status() // the int status for why connection was closed
reason() // the string reason for why connection was closed
WsErrorContext
error() // the throwable error that occurred
WsConnectContext
该类WsConnectContext不会向基础类添加任何内容WsContext

处理程序组
apiBuilder()您可以使用和方法对端点进行分组path()。这apiBuilder() 两个方法会创建一个 Javalin 的临时静态实例,以便您可以跳过处理程序前的app.或router.前缀。这相当于调用ApiBuilder.get(app/router, …),转换为app/router.get(…)。它不是保存静态信息的全局单例,因此您可以在多个位置和多个线程中安全地使用它。

您可以使用 导入所有 HTTP 方法import static io.javalin.apibuilder.ApiBuilder.*。

config.router.apiBuilder(() -> {
path(“/users”, () -> {
get(UserController::getAllUsers);
post(UserController::createUser);
path(“/{id}”, () -> {
get(UserController::getUser);
patch(UserController::updateUser);
delete(UserController::deleteUser);
});
ws(“/events”, UserController::webSocketEvents);
});
});
请注意,如果您没有自行添加,则path()路径会以 为前缀。 这意味着和是等价的。/
path(“api”, …)path(“/api”, …)

CrudHandler
这CrudHandler是一个可以在apiBuilder()调用中使用的接口:

config.router.apiBuilder(() -> {
crud(“users/{user-id}”, new UserController());
});
它实现了五种最常见的 crud 操作:

interface CrudHandler {
getAll(ctx)
getOne(ctx, resourceId)
create(ctx)
update(ctx, resourceId)
delete(ctx, resourceId)
}
验证
您可以使用 Javalin 的Validator类来获取查询、表单和路径参数,以及标头和请求正文:

ctx.queryParamAsClass(“paramName”, MyClass.class) // creates a Validator for the value of queryParam(“paramName”)
ctx.formParamAsClass(“paramName”, MyClass.class) // creates a Validator for the value of formParam(“paramName”)
ctx.pathParamAsClass(“paramName”, MyClass.class) // creates a Validator for the value of pathParam(“paramName”)
ctx.headerAsClass(“headerName”, MyClass.class) // creates a Validator for the value of header(“paramName”)
ctx.bodyValidator(MyClass.class) // creates a Validator for the value of body()
您还可以通过手动创建自己的验证器 Validator.create(clazz, value, fieldName)。

验证器 API
allowNullable() // turn the Validator into a NullableValidator (must be called first)
check(predicate, “error”) // add a check with a ValidationError(“error”) to the Validator
check(predicate, validationError) // add a check with a ValidationError to the Validator (can have args for localization)
get() // return the validated value as the specified type, or throw ValidationException
getOrThrow(exceptionFunction) // return the validated value as the specified type, or throw custom exception
getOrDefault() // return default-value if value is null, else call get()
errors() // get all the errors of the Validator (as map(“fieldName”, List))
验证示例
// VALIDATE A SINGLE QUERY PARAMETER WITH A DEFAULT VALUE /////////////////////////////////////////////
Integer myValue = ctx.queryParamAsClass(“value”, Integer.class).getOrDefault(788) // validate value
ctx.result(value) // return validated value to the client
// GET ?value=a would yield HTTP 400 - {“my-qp”:[{“message”:”TYPE_CONVERSION_FAILED”,”args”:{},”value”:”a”}]}
// GET ?value=1 would yield HTTP 200 - 1 (the validated value)
// GET ? would yield HTTP 200 - 788 (the default value)

// VALIDATE TWO DEPENDENT QUERY PARAMETERS ////////////////////////////////////////////////////////////
Instant fromDate = ctx.queryParamAsClass(“from”, Instant.class).get();
Instant toDate = ctx.queryParamAsClass(“to”, Instant.class)
.check(it -> it.isAfter(fromDate), “‘to’ has to be after ‘from’”)
.get();

// VALIDATE A JSON BODY ///////////////////////////////////////////////////////////////////////////////
MyObject myObject = ctx.bodyValidator(MyObject.class)
.check(obj -> obj.myObjectProperty == someValue, “THINGS_MUST_BE_EQUAL”)
.get();

// VALIDATE WITH CUSTOM VALIDATIONERROR ///////////////////////////////////////////////////////////////
ctx.queryParamAsClass(“param”, Integer.class)
.check({ it > 5 }, new ValidationError(“OVER_LIMIT”, Map.of(“limit”, 5)))
.get();
// GET ?param=10 would yield HTTP 400 - {“param”:[{“message”:”OVER_LIMIT”,”args”:{“limit”:5},”value”:10}]}
收集多个错误
Validator ageValidator = ctx.queryParamAsClass(“age”, Integer.class)
.check(n -> !n.contains(“-“), “ILLEGAL_CHARACTER”)

// Empty map if no errors, otherwise a map with the key “age” and failed check messages in the list.
Map<String, List> errors = ageValidator.errors();

// Merges all errors from all validators in the list. Empty map if no errors exist.
Map<String, List> manyErrors = JavalinValidation.collectErrors(ageValidator, otherValidator, …)
验证异常
当Validator抛出异常时,它被映射为:

app.exception(ValidationException::class.java) { e, ctx ->
ctx.json(e.errors).status(400)
}
您可以通过执行以下操作来覆盖此设置:

app.exception(ValidationException.class, (e, ctx) -> {
// your code
});
自定义转换器
如果需要验证未包含的类,则必须注册自定义转换器:

Javalin.create(config -> {
config.validation.register(Instant.class, v -> Instant.ofEpochMilli(Long.parseLong(v)));
});
访问管理
Javalin 曾经有一个函数式接口AccessManager,允许您设置每个端点的身份验证和/或授权。在 Javalin 6 中,它已被处理程序取代。您可以在Javalin 5 到 6 迁移指南beforeMatched中阅读更多相关信息 。

要在 Javalin 6 中管理访问,您可以执行以下操作:

app.beforeMatched(ctx -> {
var userRole = getUserRole(ctx); // some user defined function that returns a user role
if (!ctx.routeRoles().contains(userRole)) { // routeRoles are provided through the Context interface
throw new UnauthorizedResponse(); // request will have to be explicitly stopped by throwing an exception
}
});
当您声明端点时,角色就被设置了:

app.get(“/public”, ctx -> ctx.result(“Hello public”), Role.OPEN);
app.get(“/private”, ctx -> ctx.result(“Hello private”), Role.LOGGED_IN);
默认响应
Javalin 自带一个名为 的内置类HttpResponseException,可用于默认响应。如果客户端接受 JSON,则返回 JSON 对象。否则,返回纯文本响应。

app.post(“/“) { throw new ForbiddenResponse(“Off limits!”) }
如果客户端接受 JSON:

{
“title”: “Off limits!”,
“status”: 403,
“type”: “https://javalin.io/documentation#forbiddenresponse",
“details”: []
}
否则:

Forbidden
如果您愿意的话,您可以添加一些Map<String, String>详细信息。

重定向响应
返回具有默认标题的302 FoundRedirected响应。

错误请求响应
返回具有默认标题的400 Bad RequestBad request响应。

未授权的响应
返回具有默认标题的401 UnauthorizedUnauthorized响应。

禁止响应
返回具有默认标题的403 ForbiddenForbidden响应。

NotFoundResponse
返回具有默认标题的404 Not FoundNot found响应。

方法不允许响应
返回带有默认标题的405 方法不允许Method not allowed响应。

冲突反应
返回具有默认标题的409 冲突Conflict响应。

GoneResponse
返回具有默认标题的410 GoneGone响应。

内部服务器错误响应
返回具有默认标题的500 内部服务器错误Internal server error响应。

错误网关响应
返回具有默认标题的502 Bad GatewayBad gateway响应。

服务不可用响应
返回具有默认标题的503 服务不可用Service unavailable响应。

网关超时响应
返回具有默认标题的504 网关超时Gateway timeout响应。

异常映射
所有处理程序(before、endpoint、after、ws)Exception 及其子类都可能抛出Exception异常。app.exception()和app.wsException()方法为您提供了处理以下异常的方法:

// HTTP exceptions
app.exception(NullPointerException.class, (e, ctx) -> {
// handle nullpointers here
});

app.exception(Exception.class, (e, ctx) -> {
// handle general exceptions here
// will not trigger if more specific exception-mapper found
});

// WebSocket exceptions
app.wsException(NullPointerException.class, (e, ctx) -> {
// handle nullpointers here
});

app.wsException(Exception.class, (e, ctx) -> {
// handle general exceptions here
// will not trigger if more specific exception-mapper found
});
错误映射
HTTP 错误映射类似于异常映射,但它对 HTTP 状态代码而不是异常进行操作:

app.error(404, ctx -> {
ctx.result(“Generic 404 message”);
});
将它们一起使用是有意义的:

app.exception(FileNotFoundException.class, (e, ctx) -> {
ctx.status(404);
}).error(404, ctx -> {
ctx.result(“Generic 404 message”);
});
您还可以在声明错误映射器时包含内容类型:

app.error(404, “html”, ctx -> {
ctx.html(“Generic 404 message”);
});
例如,如果您想要一组用于 HTML 的错误处理程序和一组用于 JSON 的错误处理程序,这将很有用。

服务器发送事件
在 Javalin 中,服务器发送的事件(通常也称为事件源)非常简单。只需调用app.sse(),即可访问已连接的SseClient:

app.sse(“/sse”, client -> {
client.sendEvent(“connected”, “Hello, SSE”);
client.onClose(() -> System.out.println(“Client disconnected”));
client.close(); // close the client
});
离开处理程序时客户端会自动关闭,如果需要在处理程序之外使用客户端,可以使用client.keepAlive():

Queue clients = new ConcurrentLinkedQueue();

app.sse(“/sse”, client -> {
client.keepAlive();
client.onClose(() - > clients.remove(client));
clients.add(client);
});
SseClient API
sendEvent(“myMessage”) // calls emit(“message”, “myMessage”, noId)
sendEvent(“eventName”, “myMessage”) // calls emit(“eventName”, “myMessage”, noId)
sendEvent(“eventName”, “myMessage”, “id”) // calls emit(“eventName”, “myMessage”, “id”)
sendComment(“myComment”) // calls emit(“myComment”)
onClose(runnable) // callback which runs when a client closes its connection
keepAlive() // keeps the connection alive. useful if you want to keep a list of clients to broadcast to.
close() // closes the connection
terminated() // returns true if the connection has been closed
ctx // the Context from when the client connected (to fetch query-params, etc)
配置
创建 Javalin 的新实例时,可以传递一个配置对象。Javalin 的大部分配置都可以通过子配置获得,但也有一些直接的属性和函数:

Javalin.create(config -> {
config.http // The http layer configuration: etags, request size, timeout, etc
config.router // The routing configuration: context path, slash treatment, etc
config.jetty // The embedded Jetty webserver configuration
config.staticFiles // Static files and webjars configuration
config.spaRoot = // Single Page Application roots configuration
config.requestLogger // Request Logger configuration: http and websocket loggers
config.bundledPlugins // Bundled plugins configuration: enable bundled plugins or add custom ones
config.events // Events configuration
config.vue // Vue Plugin configuration
config.contextResolver // Context resolver implementation configuration
config.validation // Default validator configuration
config.useVirtualThreads // Use virtual threads (based on Java Project Loom)
config.showJavalinBanner // Show the Javalin banner in the logs
config.startupWatcherEnabled // Print warning if instance was not started after 5 seconds
config.pvt // This is “private”, only use it if you know what you’re doing

config.events(listenerConfig) // Add an event listener
config.jsonMapper(jsonMapper) // Set a custom JsonMapper
config.fileRenderer(fileRenderer) // Set a custom FileRenderer
config.registerPlugin(plugin) // Register a plugin
config.appData(key, data) // Store data on the Javalin instance

});
以下部分解释了所有可用的子配置。

Http配置
Javalin.create(config -> {
config.http.generateEtags = booleanValue; // if javalin should generate etags for dynamic responses (not static files)
config.http.prefer405over404 = booleanValue; // return 405 instead of 404 if path is mapped to different HTTP method
config.http.maxRequestSize = longValue; // the max size of request body that can be accessed without using using an InputStream
config.http.responseBufferSize = longValue; // the size of the response buffer (default 32kb)
config.http.defaultContentType = stringValue; // the default content type
config.http.asyncTimeout = longValue; // timeout in milliseconds for async requests (0 means no timeout)
config.http.strictContentTypes = booleanValue; // throw exception if e.g content-type is missing/incorrect when attempting to parse JSON

config.http.customCompression(strategy);        // set a custom compression strategy
config.http.brotliAndGzipCompression(lvl, lvl); // enable brotli and gzip compression with the specified levels
config.http.gzipOnlyCompression(lvl);           // enable gzip compression with the specified level
config.http.brotliOnlyCompression(lvl);         // enable brotli compression with the specified level
config.http.disableCompression();               // disable compression

});
上下文解析器
中的一些方法Context可以通过ContextResolvers配置类进行配置:

Javalin.create(config -> {
config.contextResolver.ip = ctx -> “custom ip”; // called by Context#ip()
config.contextResolver.host = ctx -> “custom host”; // called by Context#host()
config.contextResolver.scheme = ctx -> “custom scheme”; // called by Context#scheme()
config.contextResolver.url = ctx -> “custom url”; // called by Context#url()
config.contextResolver.fullUrl = ctx -> “custom fullUrl”; // called by Context#fullUrl()
});
Jetty配置
Javalin.create(config -> {
config.jetty.defaultHost = “localhost”; // set the default host for Jetty
config.jetty.defaultPort = 1234; // set the default port for Jetty
config.jetty.threadPool = new ThreadPool(); // set the thread pool for Jetty
config.jetty.timeoutStatus = 408; // set the timeout status for Jetty (default 500)
config.jetty.clientAbortStatus = 499; // set the abort status for Jetty (default 500)
config.jetty.multipartConfig = new MultipartConfig(); // set the multipart config for Jetty
config.jetty.modifyJettyWebSocketServletFactory(factory -> {}); // modify the JettyWebSocketServletFactory
config.jetty.modifyServer(server -> {}); // modify the Jetty Server
config.jetty.modifyServletContextHandler(handler -> {}); // modify the ServletContextHandler (you can set a SessionHandler here)
config.jetty.modifyHttpConfiguration(httpConfig -> {}); // modify the HttpConfiguration
config.jetty.addConnector((server, httpConfig) -> new ServerConnector(server)); // add a connector to the Jetty Server
});
多部分配置
ctrl+f 的关键字:FileUploadConfig
Javalin 使用标准的 Servlet 文件上传处理机制来处理多部分请求。这允许配置每个文件的最大大小、整个请求的最大大小、通过内存上传处理的文件的最大大小,以及当上传文件超过此限制时写入缓存的目录。

所有这些值都可以通过配置进行配置,如下所示

Javalin.create(config -> {
config.jetty.multipartConfig.cacheDirectory(“c:/temp”); //where to write files that exceed the in memory limit
config.jetty.multipartConfig.maxFileSize(100, SizeUnit.MB); //the maximum individual file size allowed
config.jetty.multipartConfig.maxInMemoryFileSize(10, SizeUnit.MB); //the maximum file size to handle in memory
config.jetty.multipartConfig.maxTotalRequestSize(1, SizeUnit.GB); //the maximum size of the entire multipart request
});
请求记录器配置
您可以通过调用 添加 HTTP 请求记录器config.requestLogger.http()。该方法需要Context以及完成请求所需的时间(以毫秒为单位):

Javalin.create(config -> {
config.requestLogger.http((ctx, ms) -> {
// log things here
});
});
您可以通过调用 来添加 WebSocket 记录器config.requestLogger.ws()。该方法接受与常规调用相同的参数app.ws(),并可用于记录所有类型的事件。以下示例仅展示了onMessage,但onConnect、onError和onClose均可用:

Javalin.create(config -> {
config.requestLogger.ws(ws -> {
ws.onMessage(ctx -> {
System.out.println(“Received: “ + ctx.message());
});
});
});
记录器在端点的 WebSocket 处理程序之后运行。

路由器配置
Javalin.create(config -> {
config.router.contextPath = stringValue; // the context path (ex ‘/blog’ if you are hosting an app on a subpath, like ‘mydomain.com/blog’)
config.router.ignoreTrailingSlashes = booleanValue; // treat ‘/path’ and ‘/path/‘ as the same path
config.router.treatMultipleSlashesAsSingleSlash = booleanValue; // treat ‘/path//subpath’ and ‘/path/subpath’ as the same path
config.router.caseInsensitiveRoutes = booleanValue; // treat ‘/PATH’ and ‘/path’ as the same path
});
SpaRoot配置
单页应用 (SPA) 模式类似于静态文件处理。它在端点匹配和静态文件处理之后运行。它本质上是一个非常精巧的 404 映射器,可以将任何 404 错误转换为指定的页面。您可以通过指定不同的根路径为应用程序定义多个单页处理程序。

您可以通过执行config.spaRoot.addFile(“/root”, “/path/to/file.html”)、 和/或 来启用单页模式config.spaRoot.addFile(“/root”, “/path/to/file.html”, Location.EXTERNAL)。

动态单页处理程序
您还可以使用它Handler来提供单页根(而不是静态文件):

config.spaRoot.addHandler(“/root”, ctx -> {
ctx.html(…);
});
静态文件配置
您可以通过执行以下操作来启用静态文件服务。静态资源处理在端点匹配后config.staticFiles.add(“/directory”, location)完成,这意味着您自己的 GET 端点具有更高的优先级。该过程如下所示:

run before-handlers
run endpoint-handlers
if no endpoint-handler found
run static-file-handlers
if static-file-found
static-file-handler sends response
else
response is 404
run after-handlers
对于更高级的用例,Javalin 有一个StaticFileConfig类:

Javalin.create(config -> {
config.staticFiles.add(staticFiles -> {
staticFiles.hostedPath = “/“; // change to host files on a subpath, like ‘/assets’
staticFiles.directory = “/public”; // the directory where your files are located
staticFiles.location = Location.CLASSPATH; // Location.CLASSPATH (jar) or Location.EXTERNAL (file system)
staticFiles.precompress = false; // if the files should be pre-compressed and cached in memory (optimization)
staticFiles.aliasCheck = null; // you can configure this to enable symlinks (= ContextHandler.ApproveAliases())
staticFiles.headers = Map.of(…); // headers that will be set for the files
staticFiles.skipFileFunction = req -> false; // you can use this to skip certain files in the dir, based on the HttpServletRequest
staticFiles.mimeTypes.add(mimeType, ext); // you can add custom mimetypes for extensions
});
});
您可以config.staticFiles.add(…)多次调用此方法设置多个处理程序。处理程序之间不共享任何配置。

可以通过调用 启用 WebJars config.staticFiles.enableWebjars(),它们将在 处可用/webjars/name/version/file.ext。WebJars 可以在https://www.webjars.org/上找到。所有通过 NPM 提供的资源也都可以通过 WebJars 获得。

如果您正在构建单页应用程序(SPA),您应该查看SpaRootConfig

日志记录
添加记录器
Javalin 本身不包含日志记录器,这意味着您必须添加自己的日志记录器。如果您对 Java 日志记录器不太了解/关心,最简单的解决方法是向项目添加以下依赖项:

org.slf4j slf4j-simple 2.0.17 服务器设置 Javalin 在嵌入式Jetty上运行。要启动和停止服务器,请使用start()和stop:

Javalin app = Javalin.create()
.start() // start server (sync/blocking)
.stop() // stop server (sync/blocking)
该app.start()方法会创建一个用户线程,启动服务器,然后返回。直到调用 终止该线程,程序才会退出app.stop()。

如果您想在程序退出时进行干净关机,您可以使用:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
app.stop();
}));

app.events(event -> {
event.serverStopping(() -> { /* Your code here / });
event.serverStopped(() -> { /
Your code here */ });
});
如果想要正常关机,可以使用如下方法配置服务器modifyServer:

Javalin.create(config -> {
config.jetty.modifyServer(server -> server.setStopTimeout(5_000)); // wait 5 seconds for existing requests to finish
});
设置主机
该Javalin#start方法被重载以接受主机(IP)作为第一个参数:

Javalin.create().start(“127.0.0.1”, 1235)
自定义 SessionHandler
在我们的会话教程中了解如何配置会话。

自定义码头处理程序
您可以使用处理程序链(示例)配置嵌入式 jetty 服务器,Javalin 会将其自己的处理程序附加到该链的末尾。

StatisticsHandler statisticsHandler = new StatisticsHandler();

Javalin.create(config -> {
config.server(() -> {
Server server = new Server();
server.setHandler(statisticsHandler);
return server;
})
});
SSL/HTTP2
Javalin 现在有一个 SSL 插件:https://javalin.io/plugins/ssl-helpers。建议使用此插件来设置 SSL 和 HTTP2/3,因为它比在 Jetty 中手动配置更加方便。

要在 Jetty 中手动配置 SSL 或 HTTP2,您需要使用自定义服务器。

在示例 HelloWorldSecure中可以找到带有 SSL 的自定义服务器的示例。

自定义 HTTP2 服务器的设置工作量较大,但我们有一个 repo,其中包含 Kotlin 和 Java 中功能齐全的示例服务器:javalin-http2-example

生命周期事件
Javalin 提供服务器启动/停止事件,以及添加处理程序的事件。以下代码片段展示了所有这些事件的具体运行情况:

Javalin app = Javalin.create().events(event -> {
event.serverStarting(() -> { … });
event.serverStarted(() -> { … });
event.serverStartFailed(() -> { … });
event.serverStopping(() -> { … });
event.serverStopped(() -> { … });
event.handlerAdded(handlerMetaInfo -> { … });
event.wsHandlerAdded(wsHandlerMetaInfo -> { … });
});

app.start() // serverStarting -> (serverStarted || serverStartFailed)
app.stop() // serverStopping -> serverStopped
插件
PluginJavalin 有一个插件系统,可以让你为 Javalin 添加功能。你可以扩展类并重写所需的方法来实现。

有关插件的更多信息,请参阅插件页面。

常问问题
常见问题。

请求生命周期
Javalin 请求的生命周期非常简单。以下代码片段涵盖了所有可以钩住的地方:

Javalin#before // runs first, can throw exception (which will skip any endpoint handlers)
Javalin#get/post/patch/etc // runs second, can throw exception
Javalin#error // runs third, can throw exception
Javalin#after // runs fourth, can throw exception
Javalin#exception // runs any time a handler throws (cannot throw exception)
JavalinConfig#requestLogger // runs after response is written to client
JavalinConfig#accessManager // wraps all your endpoint handlers in a lambda of your choice
速率限制
Javalin 中包含一个非常简单的速率限制器。您可以在端点Handler函数的开头调用它:

app.get(“/“, ctx -> {
NaiveRateLimit.requestPerTimeUnit(ctx, 5, TimeUnit.MINUTES); // throws if rate limit is exceeded
ctx.status(“Hello, rate-limited World!”);
});

// you can overwrite the key-function:
RateLimitUtil.keyFunction = ctx -> // uses (ip+method+endpointPath) by default
不同的端点可以有不同的速率限制。其工作原理如下:

地图的地图保存每个键的计数器。
每次请求时,该键的计数器都会增加。
如果计数器超过指定的请求数,则会引发异常。
所有计数器都会按照您指定的每个时间单位定期清除。
安卓
由于Jetty 11 无法在 Android 上运行,因此 Javalin 5+ 也不兼容,但 Javalin 4 兼容。您可以在此处
找到 Javalin 4 的文档。您可以在此处 查看 Android 上 Jetty 11+ 的运行状态。

并发
如果您的 JRE 支持 Loom 项目,newVirtualThreadPerTaskExecutor并且您设置了配置选项,Javalin 将使用 来处理请求 enableVirtualThreads。否则,QueuedThreadPool将使用具有 250 个线程的 。

每个传入的请求都由一个专用线程处理,因此所有 Handler 实现都应该是线程安全的。此默认配置允许 Javalin 处理最多 250 个并发请求,这通常绰绰有余(请记住,大多数请求的执行时间远短于 1 秒)。

如果您的应用程序涉及大量长时间运行的请求,请考虑探索异步请求或研究使用项目 Loom 设置 Javalin,详情请参阅 Loomylin 存储库。

如果您使用ctx.future()或ctx.async(),则异步任务运行时线程将被返回到线程池。因此,异步任务完成后,请求可能会由其他线程处理。

如果您不确定应用程序是否需要异步请求,那么可能没有必要。默认配置提供与 Jetty 类似的性能, 每秒可处理超过一百万个纯文本请求。

WebSocket 消息排序
WebSocket 基于 TCP 运行,因此消息将按照客户端发送的顺序到达服务器。然后,Javalin 会按顺序处理来自给定 WebSocket 连接的消息。因此,消息的处理顺序保证与客户端发送的顺序相同。

但是,不同的连接将在多个线程上并行处理,因此 WebSocket 事件处理程序应该是线程安全的。

测试

人们经常会问如何测试 Javalin 应用。由于 Javalin 只是一个库,您可以通过编程方式实例化并启动服务器。这意味着测试完全由您自己决定。/tutorials/testing 上有一个教程,其中介绍了一些不同类型的测试(单元测试、功能/集成测试、UI/端到端测试)。您可以阅读该教程来了解如何测试您的应用。

作者:Jeebiz  创建时间:2024-09-19 10:22
最后编辑:Jeebiz  更新时间:2025-05-04 00:55