Chapter Two : “Hello Javelin”
Setting up your IDE
Provide instructions and a screenshot to download IntelliJ Idea.
Creating a project
IntelliJ IDEA lets you create a Maven project or add a Maven support to any existing project.
- Launch the New Project wizard. If no project is currently opened in IntelliJ IDEA, click Create New Project on the Welcome screen: Otherwise, select File | New | Project from the main menu.
- Select Maven from the options on the left.
- Specify project’s SDK (JDK) or use a default one and an archetype if you want to use a predefined project template (configure your own archetype by clicking Add Archetype).
- Click Next.
- On the next page of the wizard, specify the following Maven basic elements that are added to the pom.xml file:
- GroupId - a package of a new project.
- ArtifactId - a name of your project.
- Version - a version of a new project. By default, this field is specified automatically.
- Click Next.
- If you are creating a project using a Maven archetype, IntelliJ IDEA displays Maven settings that you can use to set the Maven home directory and Maven repositories. Also, you can check the archetype properties. Click Next. Specify the name and location settings. Click Finish.
Setting up Maven
You should have the following code in your pom.xml:
1 <?xml version="1.0" encoding="UTF-8"?>
2 <project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst\
4 ance"
5 xsi:schemaLocation="http://maven.apache.org/POM/\
6 4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
7 <modelVersion>4.0.0</modelVersion>
8 <groupId>com</groupId>
9 <artifactId>manish</artifactId>
10 <version>1.0-SNAPSHOT</version>
11 <build>
12 <plugins>
13 <plugin>
14 <groupId>org.apache.maven.plugins</groupI\
15 d>
16 <artifactId>maven-compiler-plugin</artifa\
17 ctId>
18 <configuration>
19 <source>8</source>
20 <target>8</target>
21 </configuration>
22 </plugin>
23 </plugins>
24 </build>
25 <dependencies>
26 <dependency>
27 <groupId>io.javalin</groupId>
28 <artifactId>javalin</artifactId>
29 <version>1.7.0</version>
30 </dependency>
31 </dependencies>
32 </project>
TIP: To download source code of Javalin:
mvn dependency:sources
A simple “Hello Javelin” app
Create a class called HelloWorld and write the following code in it:
1 import io.javalin.Javalin;
2 public class HelloWorld {
3 public static void main(String[] args) {
4 Javalin app = Javalin.start(7000);
5 app.get("/", ctx -> ctx.result("Hello Javelin"));
6 }
7 }
A quick overview of the “Hello Javelin” App
Let us now go through the application describing what each component does in detail.
1 Javalin app = Javalin.start(7000);
The code above starts the application on port 7000. Let us see what internally happens when we start a Javalin app.
Javalin app internals: Startup
1 public Javalin start() {
2 if (!started) {
3 if (!hideBanner) {
4 log.info(Util.INSTANCE.javalinBanner());
5 }
6 Util.INSTANCE.printHelpfulMessageIfLoggerIsMissin\
7 g();
8 Util.INSTANCE.setNoServerHasBeenStarted(false);
9 eventManager.fireEvent(EventType.SERVER_STARTING,\
10 this);
11 try {
12 embeddedServer = embeddedServerFactory.create\
13 (new JavalinServlet(
14 contextPath,
15 pathMatcher,
16 exceptionMapper,
17 errorMapper,
18 jettyWsHandlers,
19 javalinWsHandlers,
20 logLevel,
21 dynamicGzipEnabled,
22 defaultContentType,
23 defaultCharacterEncoding,
24 maxRequestCacheBodySize
25 ), staticFileConfig);
26 log.info("Starting Javalin ...");
27 port = embeddedServer.start(port);
28 log.info("Javalin has started \\o/");
29 started = true;
30 eventManager.fireEvent(EventType.SERVER_START\
31 ED, this);
32 } catch (Exception e) {
33 log.error("Failed to start Javalin", e);
34 if (e instanceof BindException && e.getMessag\
35 e() != null) {
36 if (e.getMessage().toLowerCase().contains\
37 ("in use")) {
38 log.error("Port already in use. Make \
39 sure no other process is using port " + port + " and try \
40 again.");
41 } else if (e.getMessage().toLowerCase().c\
42 ontains("permission denied")) {
43 log.error("Port 1-1023 require elevat\
44 ed privileges (process must be started by admin).");
45 }
46 }
47 eventManager.fireEvent(EventType.SERVER_START\
48 _FAILED, this);
49 }
50 }
51 return this;
52 }
Javalin app internals:
The second line of the Javalin app reads:
1 app.get("/", ctx -> ctx.result("Hello Javelin"));
The Context instance.
The ctx represents an instance of the Context object, which contains everything you need to handle an http request.
It contains the underlying servlet-request and servlet-response, and a bunch of getters and setters. The getters operate mostly on the request-object, while the setters operate exclusively on the response object.
Context methods: A reference
1 // REQUEST METHODS
2 ctx.request();
3 // get underlying HttpServletRequest
4 ctx.anyFormParamNull("k1", "k2");
5 // returns true if any form-param is null
6 ctx.anyQueryParamNull("k1", "k2");
7 // returns true if any query-param is null
8 ctx.body();
9 // get the request body as string
10 ctx.bodyAsBytes();
11 // get the request body as byte-array
12 ctx.bodyAsClass(clazz);
13 // convert json body to object
14 ctx.formParam("key");
15 // get form param
16 ctx.formParams("key");
17 // get form param with multiple values
18 ctx.formParamMap();
19 // get all form param key/values as map
20 ctx.param("key");
21 // get a path-parameter, ex "/:id" -> param("id")
22 ctx.paramMap();
23 // get all param key/values as map
24 ctx.splat(0);
25 // get splat by nr, ex "/*" -> splat(0)
26 ctx.splats();
27 // get array of splat-values
28 ctx.attribute("key", "value");
29 // set a request attribute
30 ctx.attribute("key");
31 // get a request attribute
32 ctx.attributeMap();
33 // get all attribute key/values as map
34 ctx.basicAuthCredentials()
35 // get username and password used for basic-auth
36 ctx.contentLength();
37 // get request content length
38 ctx.contentType();
39 // get request content type
40 ctx.cookie("key");
41 // get cookie by name
42 ctx.cookieMap();
43 // get all cookie key/values as map
44 ctx.header("key");
45 // get a header
46 ctx.headerMap();
47 // get all header key/values as map
48 ctx.host();
49 // get request host
50 ctx.ip();
51 // get request up
52 ctx.isMultipart();
53 // check if request is multipart
54 ctx.mapFormParams("k1", "k2");
55 // map form params to their values, returns null if any f\
56 orm param is missing
57 ctx.mapQueryParams("k1", "k2");
58 // map query params to their values, returns null if any \
59 query param is missing
60 ctx.matchedPath();
61 // get matched path, ex "/path/:param"
62 ctx.next();
63 // pass the request to the next handler
64 ctx.path();
65 // get request path
66 ctx.port();
67 // get request port
68 ctx.protocol();
69 // get request protocol
70 ctx.queryParam("key");
71 // get query param
72 ctx.queryParams("key");
73 // get query param with multiple values
74 ctx.queryParamMap();
75 // get all query param key/values as map
76 ctx.queryString();
77 // get request query string
78 ctx.method();
79 // get request method
80 ctx.scheme();
81 // get request scheme
82 ctx.sessionAttribute("foo", "bar");
83 // set session-attribute "foo" to "bar"
84 ctx.sessionAttribute("foo");
85 // get session-attribute "foo"
86 ctx.sessionAttributeMap();
87 // get all session attributes as map
88 ctx.uploadedFile("key");
89 // get file from multipart form
90 ctx.uploadedFiles("key");
91 // get files from multipart form
92 ctx.uri();
93 // get request uri
94 ctx.url();
95 // get request url
96 ctx.userAgent();
97 // get request user agent
98
99 // RESPONSE methods
100
101 ctx.response();
102 // get underlying HttpServletResponse
103 ctx.result("result");
104 // set result (string)
105 ctx.result(inputStream);
106 // set result (stream)
107 ctx.result(future);
108 // set result (future)
109 ctx.resultString();
110 // get response result (string)
111 ctx.resultStream();
112 // get response result (stream)
113 ctx.resultFuture();
114 // get response result (future)
115 ctx.charset("charset");
116 // set response character encoding
117 ctx.header("key", "value");
118 // set response header
119 ctx.html("body html");
120 // set result and html content type
121 ctx.json(object);
122 // set result with object-as-json
123 ctx.redirect("/location");
124 // redirect to location
125 ctx.redirect("/location", 302);
126 // redirect to location with code
127 ctx.status();
128 // get response status
129 ctx.status(404);
130 // set response status
131 ctx.cookie("key", "value");
132 // set cookie with key and value
133 ctx.cookie("key", "value", 0);
134 // set cookie with key, value, and maxage
135 ctx.cookie(cookieBuilder);
136 // set cookie using cookiebuilder
137 ctx.removeCookie("key");
138 // remove cookie by key
139 ctx.removeCookie("/path", "key");
140 // remove cookie by path and key
Server Startup and Lifecyle management
To start and stop the server, use the appropriately named start() and stop() methods.
1 Javalin app = Javalin.create()
2 .start() // start server (sync/blocking)
3 .stop() // stop server (sync/blocking)
1 Javalin app = Javalin.start(7000);
Server setup: Available configurations
1 Javalin.create() // create has to be called first
2 .contextPath("/context-path")
3 // set a context path (default is "/")
4 .dontIgnoreTrailingSlashes()
5 // treat '/test' and '/test/' as different URLs
6 .defaultContentType(string)
7 // set a default content-type for responses
8 .defaultCharacterEncoding(string)
9 // set a default character-encoding for responses
10 .disableStartupBanner()
11 // remove the javalin startup banner from logs
12 .embeddedServer( ... )
13 // see section below
14 .enableCorsForOrigin("origin")
15 // enables cors for the specified origin(s)
16 .enableDynamicGzip()
17 // gzip response (if client accepts gzip and response\
18 is more than 1500 bytes)
19 .enableRouteOverview("/path")
20 // render a HTML page showing all mapped routes
21 .enableStandardRequestLogging()
22 // does requestLogLevel(LogLevel.STANDARD)
23 .enableStaticFiles("/public")
24 // enable static files (opt. second param Location.CL\
25 ASSPATH/Location.EXTERNAL)
26 .maxBodySizeForRequestCache(long)
27 // set max body size for request cache
28 .port(port)
29 // set the port
30 .start();
31 // start has to be called last
Custom Server
1 app.embeddedServer(new EmbeddedJettyFactory(() -> {
2 Server server = new Server();
3 // do whatever you want here
4 return server;
5 }));
Custom jetty handlers
You can configure your embedded jetty-server with a handler-chain, and Javalin will attach it’s own handlers to the end of this chain.
1 StatisticsHandler statisticsHandler = new StatisticsHandl\
2 er();
3
4 Javalin.create()
5 .embeddedServer(new EmbeddedJettyFactory(() -> {
6 Server server = new Server();
7 server.setHandler(statisticsHandler);
8 return server;
9 }))
10 .start();
Implementing a custom server with SSL
Implementing a custom server with SSL enabled is easy in Javalin, but not straightforward. It may require hunting around in the documentation. For your reference, here is a complete working example with SSL
1 import io.javalin.Javalin;
2 import org.eclipse.jetty.server.Connector;
3 import org.eclipse.jetty.server.Server;
4 import org.eclipse.jetty.server.ServerConnector;
5 import org.eclipse.jetty.util.ssl.SslContextFactory;
6
7 public class HelloWorldSecure {
8
9 // This is a very basic example, a better one can be \
10 found at:
11 // https://github.com/eclipse/jetty.project/blob/jett\
12 y-9.4.x/examples/embedded/src/main/java/org/eclipse/jetty\
13 /embedded/LikeJettyXml.java#L139-L163
14 public static void main(String[] args) {
15 Javalin.create()
16 .server(() -> {
17 Server server = new Server();
18 ServerConnector sslConnector = new Server\
19 Connector(server, getSslContextFactory());
20 sslConnector.setPort(443);
21 ServerConnector connector = new ServerCon\
22 nector(server);
23 connector.setPort(80);
24 server.setConnectors(new Connector[]{sslC\
25 onnector, connector});
26 return server;
27 })
28 .start()
29 .get("/", ctx -> ctx.result("Hello World")); \
30 // valid endpoint for both connectors
31 }
32
33 private static SslContextFactory getSslContextFactory\
34 () {
35 SslContextFactory sslContextFactory = new SslCont\
36 extFactory();
37 sslContextFactory.setKeyStorePath(HelloWorldSecur\
38 e.class.getResource("/keystore.jks").toExternalForm());
39 sslContextFactory.setKeyStorePassword("password");
40 return sslContextFactory;
41 }
42 }
Implementing a custom server with HTTP/2
The following is a sample server implemented with HTTP2. There is no straight-forward example easily, please use the following code below:
1 import io.javalin.Javalin;
2 import io.javalin.embeddedserver.jetty.EmbeddedJettyFacto\
3 ry;
4 import org.eclipse.jetty.alpn.ALPN;
5 import org.eclipse.jetty.alpn.server.ALPNServerConnection\
6 Factory;
7 import org.eclipse.jetty.http2.HTTP2Cipher;
8 import org.eclipse.jetty.http2.server.HTTP2ServerConnecti\
9 onFactory;
10 import org.eclipse.jetty.server.HttpConfiguration;
11 import org.eclipse.jetty.server.HttpConnectionFactory;
12 import org.eclipse.jetty.server.SecureRequestCustomizer;
13 import org.eclipse.jetty.server.Server;
14 import org.eclipse.jetty.server.ServerConnector;
15 import org.eclipse.jetty.server.SslConnectionFactory;
16 import org.eclipse.jetty.util.ssl.SslContextFactory;
17
18 public class Main {
19
20 public static void main(String[] args) {
21
22 Javalin app = Javalin.create()
23 .embeddedServer(createHttp2Server())
24 .enableStaticFiles("/public")
25 .start();
26
27 app.get("/", ctx -> ctx.result("Hello World"));
28
29 }
30
31 private static EmbeddedJettyFactory createHttp2Server\
32 () {
33 return new EmbeddedJettyFactory(() -> {
34 Server server = new Server();
35
36 ServerConnector connector = new ServerConnect\
37 or(server);
38 connector.setPort(8080);
39 server.addConnector(connector);
40
41 // HTTP Configuration
42 HttpConfiguration httpConfig = new HttpConfig\
43 uration();
44 httpConfig.setSendServerVersion(false);
45 httpConfig.setSecureScheme("https");
46 httpConfig.setSecurePort(8443);
47
48 // SSL Context Factory for HTTPS and HTTP/2
49 SslContextFactory sslContextFactory = new Ssl\
50 ContextFactory();
51 sslContextFactory.setKeyStorePath(Main.class.\
52 getResource("/keystore.jks").toExternalForm());
53 // replace with your real keystore
54 sslContextFactory.setKeyStorePassword("passwo\
55 rd");
56 // replace with your real password
57 sslContextFactory.setCipherComparator(HTTP2Ci\
58 pher.COMPARATOR);
59 sslContextFactory.setProvider("Conscrypt");
60
61 // HTTPS Configuration
62 HttpConfiguration httpsConfig = new HttpConfi\
63 guration(httpConfig);
64 httpsConfig.addCustomizer(new SecureRequestCu\
65 stomizer());
66
67 // HTTP/2 Connection Factory
68 HTTP2ServerConnectionFactory h2 = new HTTP2Se\
69 rverConnectionFactory(httpsConfig);
70 ALPNServerConnectionFactory alpn = new ALPNSe\
71 rverConnectionFactory();
72 alpn.setDefaultProtocol("h2");
73
74 // SSL Connection Factory
75 SslConnectionFactory ssl = new SslConnectionF\
76 actory(sslContextFactory, alpn.getProtocol());
77
78 // HTTP/2 Connector
79 ServerConnector http2Connector = new ServerCo\
80 nnector(server, ssl, alpn, h2, new HttpConnectionFactory(\
81 httpsConfig));
82 http2Connector.setPort(8443);
83 server.addConnector(http2Connector);
84
85 return server;
86 });
87 }
88 }
Note that you will have to generate a keystore locally using: keytool -genkey -alias mydomain -keyalg RSA -keystore keystore.jks -keysize 2048
Context Extensions:
Context extensions give Java developers a way of extending the Context object.
One of the most popular features of Kotlin is extension functions. When working with an object you don’t own in Java, you often end up making MyUtil.action(object, ...). If you, for example, want to serialize an object and set it as the result on the Context, you might do:
1 app.get("/", ctx -> MyMapperUtil.serialize(ctx, myMapper,\
2 myObject));
With context extensions you can add custom extensions on the context:
1 app.get("/", ctx -> ctx.use(MyMapper.class).serialize(obj\
2 ect)); // use MyMapper to serialize object
Context extensions have to be added before you can use them, this would typically be done in the first before filter of your app:
1 app.before(ctx -> ctx.register(MyMapper.class, new MyMapp\
2 er(ctx, otherDependency));
AccessManager for Authentication and Authorization
Javalin has a functional interface AccessManager, which let’s you set per-endpoint authentication and/or authorization. It’s common to use before-handlers for this, but per-endpoint security handlers give you much more explicit and readable code. You can implement your access-manager however you want, but here is an example implementation:
1 // Set the access-manager that Javalin should use
2 app.accessManager((handler, ctx, permittedRoles) -> {
3 MyRole userRole = getUserRole(ctx);
4 if (permittedRoles.contains(userRole)) {
5 handler.handle(ctx);
6 } else {
7 ctx.status(401).result("Unauthorized");
8 }
9 });
10
11 Role getUserRole(Context ctx) {
12 // determine user role based on request
13 // typically done by inspecting headers
14 }
15
16 enum MyRole implements Role {
17 ANYONE, ROLE_ONE, ROLE_TWO, ROLE_THREE;
18 }
19
20 app.routes(() -> {
21 get("/un-secured",
22 ctx -> ctx.result("Hello"), roles(ANYONE));
23 get("/secured",
24 ctx -> ctx.result("Hello"), roles(ROLE_ONE));
25 });
Exception Mapping
All handlers (before, endpoint, after) can throw Exception (and any subclass of Exception) The app.exception() method gives you a way of handling these exceptions:
1 app.exception(NullPointerException.class, (e, ctx) -> {
2 // handle nullpointers here
3 });
4
5 app.exception(Exception.class, (e, ctx) -> {
6 // handle general exceptions here
7 // will not trigger if more specific exception-mapper\
8 found
9 });
Javalin has a HaltException which is handled before other exceptions. It can be used to short-circuit the request-lifecycle. If you throw a HaltException in a before-handler, no endpoint-handler will fire. When throwing a HaltException you can include a status code, a message, or both:
1 throw new HaltException(); // (status\
2 : 200, message: "Execution halted")
3 throw new HaltException(401); // (status\
4 : 401, message: "Execution halted")
5 throw new HaltException("My message"); // (status\
6 : 200, message: "My message")
7 throw new HaltException(401, "Unauthorized"); // (status\
8 : 401, message: "Unauthorized")
Error Mapping
Error mapping is similar to exception mapping, but it operates on HTTP status codes instead of Exceptions:
1 app.error(404, ctx -> {
2 ctx.result("Generic 404 message")
3 });
It can make sense to use them together:
1 app.exception(FileNotFoundException.class, (e, ctx) -> {
2 ctx.status(404);
3 }).error(404, ctx -> {
4 ctx.result("Generic 404 message")
5 });
WebSockets
Javalin has a very intuitive way of handling WebSockets, similar to most node frameworks:
1 app.ws("/websocket/:path", ws -> {
2 ws.onConnect(session -> System.out.println("Connected\
3 "));
4 ws.onMessage((session, message) -> {
5 System.out.println("Received: " + message);
6 session.getRemote().sendString("Echo: " + message\
7 );
8 });
9 ws.onClose((session, statusCode, reason) -> System.ou\
10 t.println("Closed"));
11 ws.onError((session, throwable) -> System.out.println\
12 ("Errored"));
13 });
The WsSession object wraps Jetty’s Session and adds the following methods:
1 session.send("message")
2 // send a message to session remote (the ws client)
3 session.queryString()
4 // get query-string from upgrade-request
5 session.queryParam("key")
6 // get query-param from upgrade-request
7 session.queryParams("key")
8 // get query-params from upgrade-request
9 session.queryParamMap()
10 // get query-param-map from upgrade-request
11 session.mapQueryParams("k1", "k2")
12 // map query-params to values (only useful in kotlin)
13 session.anyQueryParamNull("k1", "k2")
14 // check if any query-param from upgrade-request is null
15 session.param("key")
16 // get a path-parameter, ex "/:id" -> param("id")
17 session.paramMap()
18 // get all param key/values as map
19 session.header("key")
20 // get a header
21 session.headerMap()
22 // get all header key/values as map
23 session.host()
24 // get request host
Lifecycle events
Javalin has five lifecycle events: SERVER_STARTING, SERVER_STARTED, SERVER_START_FAILED, SERVER_STOPPING and SERVER_STOPPED. The snippet below shows all of them in action:
1 Javalin app = Javalin.create()
2 .event(EventType.SERVER_STARTING, e -> { ... })
3 .event(EventType.SERVER_STARTED, e -> { ... })
4 .event(EventType.SERVER_START_FAILED, e -> { ... })
5 .event(EventType.SERVER_STOPPING, e -> { ... })
6 .event(EventType.SERVER_STOPPED, e -> { ... });
7
8 app.start(); // SERVER_STARTING -> (SERVER_STARTED || SER\
9 VER_START_FAILED)
10 app.stop(); // SERVER_STOPPING -> SERVER_STOPPED
Adding a logger:
If you’re reading this, you’ve probably seen the following message while running Javalin:
1 SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerB\
2 inder".
3 SLF4J: Defaulting to no-operation (NOP) logger implementa\
4 tion
5 SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBi\
6 nder for further details.
1 <dependency>
2 <groupId>org.slf4j</groupId>
3 <artifactId>slf4j-simple</artifactId>
4 <version>1.7.25</version>
5 </dependency>
Asynchronous requests
Javalin 1.6.0 introduced future results. While the default threadpool (200 threads) is enough for most use cases, sometimes slow operations should be run asynchronously. Luckily it’s very easy in Javalin, just pass a CompletableFuture to ctx.result():
1 import io.javalin.Javalin
2
3 fun main(args: Array<String>) {
4 val app = Javalin.start(7000)
5 app.get("/") { ctx -> ctx.result(getFuture()) }
6 }
7
8 // hopefully your future is less pointless than this:
9 private fun getFuture() = CompletableFuture<String>().app\
10 ly {
11 Executors.newSingleThreadScheduledExecutor()
12 .schedule({ this.complete("Hello World!") }, 1, Time\
13 Unit.SECONDS)
14 }
You can only set future results in endpoint handlers (get/post/put/etc). After-handlers, exception-handlers and error-handlers run like you’d expect them to after the future has been resolved or rejected.
Configuring JSON Mapper and Jackson
The JSON mapper can be configured like this:
1 Gson gson = new GsonBuilder().create();
2 JavalinJsonPlugin.setJsonToObjectMapper(gson::fromJson);
3 JavalinJsonPlugin.setObjectToJsonMapper(gson::toJson);
Configuring Jackson
The JSON mapper uses Jackson by default, which can be configured by calling:
1 JavalinJacksonPlugin.configure(objectMapper)
Note that these are global settings, and can’t be configured per instance of Javalin.
Views and Templates
Javalin currently supports five template engines, as well as markdown:
1 ctx.renderThymeleaf("/templateFile", model("firstName", "\
2 John", "lastName", "Doe"))
3 ctx.renderVelocity("/templateFile", model("firstName", "J\
4 ohn", "lastName", "Doe"))
5 ctx.renderFreemarker("/templateFile", model("firstName", \
6 "John", "lastName", "Doe"))
7 ctx.renderMustache("/templateFile", model("firstName", "J\
8 ohn", "lastName", "Doe"))
9 ctx.renderJtwig("/templateFile", model("firstName", "John\
10 ", "lastName", "Doe"))
11 ctx.renderMarkdown("/markdownFile")
12 // Javalin looks for templates/markdown files in src/reso\
13 urces
Configure:
1 JavalinThymeleafPlugin.configure(templateEngine)
2 JavalinVelocityPlugin.configure(velocityEngine)
3 JavalinFreemarkerPlugin.configure(configuration)
4 JavalinMustachePlugin.configure(mustacheFactory)
5 JavalinJtwigPlugin.configure(configuration)
6 JavalinCommonmarkPlugin.configure(htmlRenderer, markdownP\
7 arser)
Note that these are global settings, and can’t be configured per instance of Javalin.