Javalin 快速开始

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

添加依赖项:

<dependency>
    <groupId>io.javalin</groupId>
    <artifactId>javalin</artifactId>
    <version>6.7.0</version>
</dependency>

如果你想使用 Javalin 和测试工具 Jackson 和 Logback, 可以使用工件 id javalin-bundle 来代替 Javalin。

开始编码:

import io.javalin.Javalin;

void main() {
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}
一个 Handler 实现,ex ctx -> {…},MyClass 实现 Handler, 等等。
Handler 接口具有 void 返回类型。可以使用 ctx.result (result)、ctx.json (obj) 或 ctx.future (future) 等方法来设置将返回给用户的响应。

如果为同一路径添加多个前后 Handler, 它们将按照添加的顺序执行。这对于添加身份验证、缓存、日志记录等可能很有用。

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

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

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

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

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

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

app.post(“/input”, ctx -> {
// some code
ctx.status(201);
});
处理程序路径可以包含 path 参数。这些参数可以通过 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());
});
然而,无法提取通配符的值。如果需要这种行为,可以使用接受 path-parameter ( 而不是 {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-request 所需的一切。它包含底层的 servlet-request 和 servlet-response, 以及一堆 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#create 在 Javalin 实例上注册,然后通过 Context 中的 appData (…) 方法访问。你需要为数据创建一个类型化键,然后将其注册到 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();
将键存储为静态字段可能很有帮助,但你也可以在每次需要时重新创建该键。

Cookie Store
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, 因此对 serverTwoApp 的 get 请求将能够检索在发帖中传递给 serverOneApp 的信息。

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

WebSocket
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
使用 app.wsBefore (path,handler) 声明 WebSocket 终结点。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”));
});
WsAfter
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 (…)。它不是一个保存静态信息的全局单例,因此可以在多个位置和多个线程中安全地使用它。

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

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);
});
});
请注意:使用(除非你自己添加它)。这意味着……以及是等价的。

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

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

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, …)
ValidationException
当验证器抛出时,它被映射为:

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 中,这已被 preMatched 处理程序所取代。您可以在 Javalin 5 到 6 的迁移指南中阅读更多关于此的内容。

要在 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> 的详细信息。

RedirectResponse
返回一个默认标题为 Redirected 的 302 Found 响应。

BadRequestResponse
返回一个 400 Bad Request 响应,默认标题为 Bad Request。

UnauthorizedResponse
返回一个 401 未经授权的响应,默认标题为 Unauthorized。

ForbiddenResponse
返回一个 403 禁止响应,默认标题为 Forbidden。

NotFoundResponse
返回一个 404 Not Found 响应,其默认标题为 Not Found。

MethodNotAllowedResponse
返回 405 MethodNotAllowed 响应,默认标题为 Method not allowed。

ConflictResponse
返回 409 Conflict 响应,默认标题为 Conflict。

GoneResponse
返回 410 Gone 响应,默认标题为 Gone。

InternalServerErrorResponse
返回默认标题为 “内部服务器错误” 的 500 内部服务器错误响应。

Bad GatewayResponse
返回默认标题为 Bad Gateway 的 502 Bad Gateway 响应。

Service UnavailableResponse
返回默认标题为 “Service Unavailable” 的 503 Service Unavailable 响应。

GatewayTimeoutResponse
返回一个 504 Gateway Timeout 响应,其默认标题为 Gateway Timeout。

异常映射
所有处理程序 (pre、end、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 Error 映射与 Exception 映射类似,但它是在 HTTP 状态码而不是 Exception 上进行操作的:

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);
});
SeseClient 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 的新实例时传递 config 对象。Javalin 的大多数配置都可以通过 subconfig 获取,但也有一些直接的属性和函数:

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

});
所有可用的子配置将在以下部分进行说明。

HttpConfig
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

});
ContextResolvers
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()
});
JettyConfig
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
});
MultipartConfig
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
});
RequestLoggerConfig
可以通过调用 config.requestLogger.http () 来添加 HTTP 请求记录器。该方法接收一个上下文以及完成请求所需的时间 (以毫秒为单位):

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

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

RouterConfig
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
});
SpaRootConfig
单页应用程序 (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(…);
});
StaticFileConfig
您可以通过运行 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 方法被重载,以接受 Host (IP) 作为第一个参数:

Javalin.create().start(“127.0.0.1”, 1235)
自定义 SessionHandler
阅读我们的会话教程中关于如何配置会话的内容。

自定义 jetty 处理程序
您可以使用处理程序链 (示例) 配置嵌入式 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 服务器需要更多的工作,但我们在 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
插件
Javalin 有一个插件系统,它允许你通过扩展 Plugin 类并覆盖你感兴趣的方法来为 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
不同的端点可能有不同的速率限制。它的工作原理如下:

映射映射包含每个键的计数器。
对于每个请求,该键的计数器都会递增。
如果计数器超过指定的请求数,将抛出一个异常。
在你指定的每个时间单位上,所有计数器都会定期清除。
Android
由于Jetty 11 无法在 Android 上工作, Javalin 5 + 也不兼容,但 Javalin 4 可以。你可以找到 Javalin 4 的文档这里你可以在 Android 上查看 Jetty 11 + 的状态这里.

并发性
如果你的 JRE 支持项目 Looom,Javalin 将使用一个新的 VirtualThreadPerTaskExecutor 来服务请求,前提是你设置了 enableVirtualThreads 配置选项。否则,将使用一个包含 250 个线程的 QueuedThreadPool。

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

如果你的应用程序涉及大量长时间运行的请求,可以考虑探索异步请求,或者研究如何使用 Loomylin 仓库中详细介绍的项目 Looom 设置 Javalin。

在使用 ctx.future () 或 ctx.async () 的情况下,当异步任务运行时,线程将返回到线程池。因此,当异步任务完成时,请求可能由另一个线程处理。

如果你不确定你的应用程序是否需要异步请求,这可能是不必要的。默认配置提供与 Jetty 类似的性能,后者每秒可以处理超过一百万个明文请求。

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

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

测试
人们经常问如何测试 Javalin 应用。由于 Javalin 只是一个库,你可以通过编程方式实例化和启动服务器。这意味着测试实际上取决于你自己。在 /tutorials/testing 中有一个教程,它涵盖了一些不同类型的测试 (单元测试、功能 / 集成测试、UI / 端到端测试)。你可以阅读它来获得一些关于如何测试你的应用的想法。

Javadoc
Javadoc.io 上有一个 Javadoc。如果可以的话,请为 Javadoc 做出贡献。

部署
要部署 Javalin, 只需创建一个包含依赖项的 jar, 然后使用 java -jar filename.jar 启动该 jar。就这样。Javalin 有一个嵌入式服务器,所以你不需要应用程序服务器。

将 Javalin 部署到:

Heroku (教程)
AWS/Lambda (示例)
其他 Web 服务器
Ctrl+f:“无 jetty”、“tomcat”、“standalone”、“servlet container”、“war”。
Javalin 主要用于嵌入式 Jetty 服务器,但如果你想在其他 Web 服务器 (如 Tomcat) 上运行 Javalin, 可以使用 Maven 或 Gradle 来排除 Jetty, 并将 Javalin 作为 servlet 附加。

上传
上传的文件可以通过 ctx.uploadedFiles () 轻松访问:

app.post(“/upload”, ctx -> {
ctx.uploadedFiles(“files”).forEach(uploadedFile ->
FileUtil.streamToFile(uploadedFile.content(), “upload/“ + uploadedFile.filename()));
});
相应的 HTML 可能看起来像这样:

异步请求 Ctrl+f 的同义词:异步、CompletableFuture、Future、Concurrent、Concurrency 尽管默认的 ThreadPool (250 个线程) 对于大多数使用案例来说已经足够,但有时缓慢的操作应该异步运行。幸运的是,在 Javalin 中这非常简单,只需将 Supplier 传递给 ctx.future () 即可。Javalin 会自动在同步和异步模式之间切换,以处理不同的任务。

使用 Future
让我们看一个真实的例子。假设我们有一个随机的猫咪事实 API, 想要代表客户端调用它。我们首先创建一个简单的方法来调用 API, 该方法返回 CompletableFuture, 它将解析为一个猫咪事实或一个错误。这可以通过使用 Java 的原生 HttpClient 来实现:

private static CompletableFuture<HttpResponse> getRandomCatFactFuture() {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(“https://catfact.ninja/fact"))
.build();
return httpClient.sendAsync(request, ofString());
}
现在我们可以在 Javalin 应用中使用这个方法,以异步方式向客户端返回 cat fact:

app.get(“/cat-facts”, ctx -> {
ctx.future(() -> {
return getRandomCatFactFuture()
.thenAccept(response -> ctx.html(response.body()).status(response.statusCode()))
.exceptionally(throwable -> {
ctx.status(500).result(“Could not get cat facts” + throwable.getMessage());
return null;
})
});
});
通过调用 ctx.future (supplier), 你并没有将 Javalin 置于异步状态。这是一个简单的 setter 方法,它使 Javalin 能够调用给定的 supplier, 并在适当的时候切换到异步模式。

Ctx.future () 方法在使用基于 CompletableFuture 的 API (如 Java 的 HttpClient) 时效果很好,但如果你有长时间运行的任务,而这些任务并非基于 CompletableFuture, 你可能需要尝试使用 ctx.async (runnable) 方法 (参见下一节)。

异步执行阻塞任务
如果想在 ThreadPool 服务器之外执行阻塞任务,可以使用 ctx.async ()。下面的代码片段显示了该方法的可用重载:

async(runnableTask) // Javalin’s default executor, no timeout or timeout callback
async(timeout, onTimeout, runnableTask) // Javalin’s default executor, custom timeout handling
async(executor, timeout, onTimeout, runnableTask) // custom everything!
Javalin 将立即启动一个异步上下文,并在专用的执行器服务上运行任务。任务完成后,它将恢复正常的请求流 (后处理程序、请求日志记录)。

下面的代码片段显示了一个完整的示例,其中包含自定义超时、超时处理程序和任务:

app.get(“/async”, ctx -> {
ctx.async(
1000, // timeout in ms
() -> ctx.result(“Request took too long”), // timeout callback
() -> ctx.result(someSlowResult) // some long running task
);
});
配置 JSONMapper
要配置 JsonMapper, 需要向 config.jsonMapper () 传递一个实现 JsonMapper 接口的对象。

JsonMapper 接口有四个可选方法:

String toJsonString(Object obj, Type type) { // basic method for mapping to json
InputStream toJsonStream(Object obj, Type type) { // more memory efficient method for mapping to json
writeToOutputStream(Stream<*> stream, OutputStream outputStream) { // most memory efficient method for mapping to json
T fromJsonString(String json, Type targetType) { // basic method for mapping from json
T fromJsonStream(InputStream json, Type targetType) { // more memory efficient method for mapping from json
默认 JSON 映射器 (Jackson)
Javalin 使用 Jackson 作为默认 JSON 映射器。它是一个快速且功能丰富的映射器,如果类路径中有以下模块,它们将被启用:

com.fasterxml.jackson.module.kotlin.KotlinModule // Kotlin support
com.fasterxml.jackson.datatype.jsr310.JavaTimeModule // Java date/time support
org.ktorm.jackson.KtormModule // Ktorm support
com.fasterxml.jackson.datatype.eclipsecollections.EclipseCollectionsModule // Eclipse Collections support
如果您需要进一步配置,可以像这样更新默认设置:

config.jsonMapper(new JavalinJackson().updateMapper(mapper -> {
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
});
GSON 示例
Gson gson = new GsonBuilder().create();
JsonMapper gsonMapper = new JsonMapper() {
@Override
public String toJsonString(@NotNull Object obj, @NotNull Type type) {
return gson.toJson(obj, type);
}

@Override
public <T> T fromJsonString(@NotNull String json, @NotNull Type targetType) {
    return gson.fromJson(json, targetType);
}

};
Javalin app = Javalin.create(config -> config.jsonMapper(gsonMapper)).start(7070);
向 Javalin 添加其他 Servlet 和 Filter
Javalin 被设计用于处理 Jetty Server 上运行的其他 Servlet 和 Filter 实例。过滤器的添加非常直接,因为它们不会完成请求。如果你需要添加 Servlet,repo 中有一个示例:/src/test/java/io/javalin/examples/HelloWorldServlet.java#L21-L29

您也可以使用它来构建简单的代理,使用作为 Jetty 一部分的 AsyncProxyServlet:

// Add org.eclipse.jetty:jetty-proxy to maven/gradle dependencies (e.g Javalin 5.3.2 uses Jetty 11.0.13)
Javalin.create(config -> {
config.jetty.modifyServletContextHandler(handler -> {
ServletHolder proxyServlet = new ServletHolder(AsyncProxyServlet.Transparent.class);
proxyServlet.setInitParameter(“proxyTo”, “https://javalin.io/");
proxyServlet.setInitParameter(“prefix”, “/proxy”);
handler.addServlet(proxyServlet, “/proxy/*”);
});
}).start(7000);
打开 http://localhost:7000/proxy/ 后,你会看到 Javalin 网站 (但由于文件路径的原因,样式有所损坏)。

视图和模板
每个 Javalin 实例都附加了一个 FileRenderer。FileRenderer 接口有一个方法:

String render(String filePath, Map<String, Object> model, Context context)
当调用 Context#render 时,会调用这个方法。可以通过传递给 Javalin.create () 的配置来配置它:

config.fileRenderer((filePath, model, context) -> “Rendered template”);
Javalin 的默认 FileRenderer 是一个名为 JavalinRenderer 的单元,更多信息请参见下一节。

默认实现
Javalin 提供了一个包含多个模板引擎的构件,称为 javalin-rendering, 它遵循与 javalin 构件相同的版本。你可以在 /plugins/rendering 中了解更多关于它的信息。

Vue 支持 (JavalinVue)
如果你不想处理 NPM 和前端构建,Javalin 支持简化 Vue.js 开发。这需要你创建一个布局模板,地址为 src/main/resources/vue/layout.html:

@componentRegistration
@routeComponent
当你将.vue 文件放入 src/main/resources/vue 中时,Javalin 会扫描文件夹并在 标签中注册组件。

Javalin 还会在 Vue 实例中放置 path 参数,你可以像这样访问它:

要将路径映射到 Vue 组件,可以使用 VueComponent 类:

get(“/messages”, VueComponent(“inbox-view”))
get(“/messages/{user}”, VueComponent(“thread-view”))
这将给你带来现代前端架构的诸多好处,而缺点却很少。

/plugins/javalinvue 有更广泛的文档,/tutorials/simple-frontends-with-javalin-and-vue 有更深入的教程。

Jetty 调试日志
如果在 DEBUG 日志中遇到 TimeoutException 和 ClosedChannelException, 这没什么可担心的。通常,浏览器会保持 HTTP 连接打开,直到服务器终止它。当这种情况发生时,由服务器的 idleTimeout 设置决定,在 Jetty/Javalin 中默认设置为 30 秒。这不是一个 bug。

Java 语言错误处理
Javalin 为 java.lang.Error 提供了一个默认错误处理程序,该程序会记录错误并返回 500。可以使用私有配置覆盖默认错误处理程序:

Javalin.create( cfg -> {
cfg.pvt.javaLangErrorHandler((res, error) -> {
res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.getCode());
JavalinLogger.error(“Exception occurred while servicing http-request”, error);
});
});
我的世界
Ctrl+f 的关键词:Bukkit、Spigot、Bungee Cord、Bungee Cord、Waterfall、Waterfall、Paper
许多人在《我的世界》服务器上使用 Javalin, 但他们经常在 Jetty 和 WebSocket 上遇到问题。

如果你正在使用 Javalin 和 Minecraft 服务器,请考虑参考我们的 Minecraft 教程。

重新定位
使用 relocate 不是必要的,但它很容易与其他插件依赖项发生冲突。如果这是一个公开发布的插件,建议使用这个步骤让 Javalin 在不同的 Minecraft Server 上运行。

通常,jetty 会导致冲突,你可以在添加 shadow-jar gradle 插件后,在 build.gradle 脚本中添加以下代码:

shadowJar {
relocate ‘org.eclipse.jetty’, ‘shadow.org.eclipse.jetty’
}
自定义类加载器
如果你遇到一些依赖关系缺失错误,如 java.lang.NoClassDefFoundError 和 java.lang.ClassNotFoundException, 请尝试通过以下方式解决它:

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(RemoteAPI.class.getClassLoader());
Javalin app = Javalin.create().start(PORT);
Thread.currentThread().setContextClassLoader(classLoader);
RemoteAPI 通常可以使用插件主类的类加载器。在 Bukkit 和 Spigot 上,它是一个扩展了 org.bukkit.plugin.java#JavaPlugin 的类;在 BungeeCord 和 WaterFall 上,它是一个扩展了 net.md_5.bungee.api.plugin#Plugin 的类。通过 {你插件的主类} class.getClassLoader () 获取它。

切换类加载程序后,你可能仍然会收到 Javalin 发来的缺失依赖关系错误。你只需要按照 Javalin 日志中的提示添加相应的依赖关系即可。

相关问题
Https://github.com/javalin/javalin/issues/358 (包含解决方案)
Https://github.com/javalin/javalin/issues/232
Https://github.com/javalin/javalin/issues/1462

作者:Jeebiz  创建时间:2024-09-19 10:22
最后编辑:Jeebiz  更新时间:2025-11-23 17:51