jooby set -w ~/Source
+jooby set -w ~/Source
diff --git a/index.html b/index.html index c4f1364..5e7e793 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ - +
Exception
to HttpProblem
Looking for a previous version?
@@ -1029,7 +1046,7 @@import io.jooby.Jooby;
+import io.jooby.Jooby;
public class App extends Jooby {
@@ -1040,9 +1057,9 @@
+}
import io.jooby.annotation.*;
+import io.jooby.annotation.*;
public class MyController {
@@ -1125,9 +1142,9 @@
+}
Download jooby-cli.zip
+Download jooby-cli.zip
Unzip jooby-cli.zip
in your user home directory (or any other directory you prefer)
jooby set -w ~/Source
+jooby set -w ~/Source
jooby> create myapp
+jooby> create myapp
jooby> create myapp --kotlin
+jooby> create myapp --kotlin
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-kotlin</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
jooby> create myapp --gradle
+jooby> create myapp --gradle
jooby> create myapp --gradle --kotlin
+jooby> create myapp --gradle --kotlin
jooby> create myapp --mvc
+jooby> create myapp --mvc
jooby> create myapp --server undertow
+jooby> create myapp --server undertow
jooby> create myapp --stork
+jooby> create myapp --stork
jooby> create myapp --docker
+jooby> create myapp --docker
jooby> create myapp -i
+jooby> create myapp -i
{
+{
get("/", ctx -> "Snippet");
-}
+}
{
+{
get("/foo", ctx -> "Foo")
.attribute("foo", "bar");
-}
+}
{
+{
use(next -> ctx -> {
User user = ...;
String role = ctx.getRoute().attribute("Role");
@@ -1522,9 +1539,9 @@
+}
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
+@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {
String value();
@@ -1564,9 +1581,9 @@
+}
{
+{
get("/foo", ctx -> "Foo");
-}
+}
{
+{
(1)
get("/user/{id}", ctx -> {
int id = ctx.path("id").intValue(); (2)
return id;
});
-}
+}
{
+{
(1)
get("/file/{file}.{ext}", ctx -> {
String filename = ctx.path("file").value(); (2)
String ext = ctx.path("ext").value(); (3)
return filename + "." + ext;
});
-}
+}
{
+{
(1)
get("/profile/{id}?", ctx -> {
String id = ctx.path("id").value("self"); (2)
return id;
});
-}
+}
{
+{
(1)
get("/user/{id:[0-9]+}", ctx -> {
int id = ctx.path("id").intValue(); (2)
return id;
});
-}
+}
{
+{
(1)
get("/articles/*", ctx -> {
String catchall = ctx.path("*").value(); (2)
@@ -1803,9 +1820,9 @@ (3)
return path;
});
-}
+}
{
+{
get("/user/{id}", ctx -> ctx.path("id").value()); (1)
get("/user/me", ctx -> "my profile"); (2)
@@ -1890,9 +1907,9 @@ (3)
get("/users", ctx -> "new users"); (4)
-}
+}
interface Filter {
+interface Filter {
Handler apply(Handler next);
-}
+}
{
+{
use(next -> ctx -> {
long start = System.currentTimeMillis(); (1)
@@ -1967,9 +1984,9 @@ <
get("/", ctx -> {
return "filter";
});
-}
+}
interface Before {
+interface Before {
void apply(Context ctx);
-}
+}
{
+{
before(ctx -> {
ctx.setResponseHeader("Server", "Jooby");
});
@@ -2053,9 +2070,9 @@ <
get("/", ctx -> {
return "...";
});
-}
+}
interface After {
+interface After {
void apply(Context ctx, Object result, Throwable failure);
-}
+}
{
+{
after((ctx, result, failure) -> {
System.out.println(result); (1)
ctx.setResponseHeader("foo", "bar"); (2)
@@ -2146,9 +2163,9 @@
+}
{
+{
after((ctx, result, failure) -> {
if (ctx.isResponseStarted()) {
// Don't modify response
@@ -2193,9 +2210,9 @@
+}
{
+{
after((ctx, result, failure) -> {
if (failure == null) {
db.commit(); (1)
@@ -2237,9 +2254,9 @@ (2)
}
});
-}
+}
{
+{
after((ctx, result, failure) -> {
if (failure instanceOf MyBusinessException) {
ctx.send("Recovering from something"); (1)
}
});
-}
+}
{
+{
after((ctx, result, failure) -> {
...
throw new AnotherException();
@@ -2307,9 +2324,9 @@ (1)
Throwable anotherException = failure.getSuppressed()[0]; (2)
});
-}
+}
{
+{
use(next -> ctx -> {
long start = System.currentTimeInMillis();
ctx.onComplete(context -> { (1)
@@ -2362,9 +2379,9 @@
+}
{
+{
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
@@ -2417,9 +2434,9 @@ (1)
get("/2", ctx -> 2); (2)
-}
+}
{
+{
use("/*", (req, rsp, chain) -> {
// remote call, db call
});
// ...
-}
+}
{
+{
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
@@ -2547,9 +2564,9 @@ <
});
get("/2", ctx -> 2); (2)
-}
+}
{
+{
// Increment +1
use(next -> ctx -> {
Number n = (Number) next.apply(ctx);
@@ -2615,9 +2632,9 @@ (3)
-}
+}
{
+{
routes(() -> {
get("/", ctx -> "Hello");
});
-}
+}
public class Foo extends Jooby {
+public class Foo extends Jooby {
{
get("/foo", Context::getRequestPath);
}
@@ -2785,9 +2802,9 @@ (3)
}
-}
+}
public class Foo extends Jooby {
+public class Foo extends Jooby {
{
get("/foo", Context::getRequestPath);
}
@@ -2838,9 +2855,9 @@ (1)
}
-}
+}
public class Foo extends Jooby {
+public class Foo extends Jooby {
{
get("/foo", ctx -> ...);
}
@@ -2894,9 +2911,9 @@ (2)
}
-}
+}
{
+{
Foo foo = new Foo();
install(() -> foo); // Won't work
-}
+}
{
+{
install(() -> new Foo()); // Works!
-}
+}
public class V1 extends Jooby {
+public class V1 extends Jooby {
{
get("/api", ctx -> "v1");
}
@@ -2988,9 +3005,9 @@ (2)
}
-}
+}
<form method="post" action="/form">
+<form method="post" action="/form">
<input type="hidden" name="_method" value="put">
-</form>
+</form>
import io.jooby.Jooby;
+import io.jooby.Jooby;
...
{
@@ -3059,9 +3076,9 @@ (2)
});
-}
+}
import io.jooby.Jooby;
+import io.jooby.Jooby;
...
{
setHiddenMethod(ctx -> ctx.header("X-HTTP-Method-Override").toOptional()); (1)
-}
+}
import io.jooby.Jooby;
+import io.jooby.Jooby;
...
{
@@ -3137,9 +3154,9 @@ (5)
...
});
-}
+}
{
+{
get("/", ctx -> { /* do important stuff with variable 'ctx'. */ });
-}
+}
{
+{
setContextAsService(true);
-}
+}
application.lang = en, en-GB, de
+application.lang = en, en-GB, de
{
+{
setLocales(Locale.GERMAN, new Locale("hu", "HU"));
-}
+}
{
+{
get("/{id}" ctx -> ctx.path("id").value()); (1)
get("/@{id}" ctx -> ctx.path("id").value()); (2)
@@ -3414,9 +3431,9 @@ (4)
get("/{id:[0-9]+}", ctx -> ctx.path("id)) (5)
-}
+}
{
+{
get("/{name}", ctx -> {
String pathString = ctx.getRequestPath(); (1)
@@ -3469,9 +3486,9 @@
+}
FileUpload pic = ctx.file("pic"); (1)
+ FileUpload pic = ctx.file("pic"); (1)
List<FileUpload> pic = ctx.files("pic"); (2)
- List<FileUpload> files = ctx.files(); (3)
+ List<FileUpload> files = ctx.files(); (3)
Session session = ctx.session(); (1)
+ Session session = ctx.session(); (1)
- String attribute = ctx.session("attribute").value(); (2)
+ String attribute = ctx.session("attribute").value(); (2)
get("/", ctx -> {
+ get("/", ctx -> {
return ctx.flash("success").value("Welcome!"); (3)
});
post("/save", ctx -> {
ctx.flash().put("success", "Item created"); (1)
return ctx.sendRedirect("/"); (2)
- });
+ });
{
+{
setFlashCookie(new Cookie("myflash").setHttpOnly(true));
// or if you're fine with the default name
getFlashCookie().setHttpOnly(true);
-}
+}
get("/{foo}", ctx -> {
+get("/{foo}", ctx -> {
String foo = ctx.lookup("foo", ParamSource.QUERY, ParamSource.PATH).value();
return "foo is: " + foo;
});
@@ -4055,9 +4072,9 @@
+});
get("/{foo}", ctx -> {
+get("/{foo}", ctx -> {
List<Certificate> certificates = ctx.getClientCertificates(); (1)
Certificate peerCertificate = certificates.get(0); (2)
-});
+});
{
+{
get("/", ctx -> {
String name = ctx.query("name").value(); (1)
@@ -4154,9 +4171,9 @@ (4)
...
});
-}
+}
{
+{
get("/search", ctx -> {
String q = ctx.query("q").value("*:*"); (1)
return q;
@@ -4273,9 +4290,9 @@ (2)
return q;
});
-}
+}
{
+{
get("/", ctx -> {
Value user = ctx.query("user"); (1)
String name = user.get("name").value(); (2)
@@ -4443,9 +4460,9 @@ (4)
...
}}
-}
+}
class Member {
+class Member {
public final String firstname;
public final String lastName;
@@ -4547,11 +4564,11 @@ PO
this.id = id;
this.members = members;
}
-}
+}
{
+{
get("/", ctx -> {
Member member = ctx.query(Member.class);
...
});
-}
+}
{
+{
get("/", ctx -> {
Member member = ctx.query("member").to(Member.class);
...
});
-}
+}
{
+{
get("/", ctx -> {
List<Member> members = ctx.query().toList(Member.class);
...
});
-}
+}
{
+{
get("/", ctx -> {
Group group = ctx.query(Group.class);
...
});
-}
+}
class Member {
+class Member {
public final String firstname;
public final String lastname;
@@ -4701,9 +4718,9 @@ PO
public Member(@Named("first-name") String firstname, @Named("last-name") String lastname) {
....
}
-}
+}
{
+{
post("/string", ctx -> {
String body = ctx.body().value(); (1)
...
@@ -4736,9 +4753,9 @@ (3)
...
});
-}
+}
public interface MessageDecoder {
+public interface MessageDecoder {
<T> T decode(Context ctx, Type type) throws Exception;
-}
+}
{
+{
FavoriteJson lib = new FavoriteJson(); (1)
decoder(MediaType.json, (ctx, type) -> { (2)
@@ -4809,9 +4826,9 @@ (5)
});
-}
+}
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
{
+{
get("/", ctx -> {
ctx.setResponseCode(200); (1)
@@ -4899,9 +4916,9 @@ <
return "Response"; (4)
});
-}
+}
public interface MessageEncoder {
+public interface MessageEncoder {
byte[] encode(@NonNull Context ctx, @NonNull Object value) throws Exception;
-}
+}
{
+{
FavoriteJson lib = new FavoriteJson(); (1)
encoder(MediaType.json, (ctx, result) -> { (2)
@@ -4971,9 +4988,9 @@ (6)
});
-}
+}
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jackson</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
tasks.withType(JavaCompile) {
+tasks.withType(JavaCompile) {
options.compilerArgs += [
'-parameters',
'-Ajooby.incremental=true',
'-Ajooby.services=true'
]
-}
+}
import io.jooby.annotation.*;
+import io.jooby.annotation.*;
@Path("/mvc") (1)
public class Controller {
@@ -5130,9 +5147,9 @@
+}
public class App extends Jooby {
+public class App extends Jooby {
{
mvc(new MyController_());
}
@@ -5199,15 +5216,15 @@
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@HeaderParam String token) { (1)
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@HeaderParam("Last-Modified-Since") long lastModifiedSince) {
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@CookieParam String token) { (1)
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@CookieParam("token-id") String tokenId) {
...
}
-}
+}
public class MyController {
+public class MyController {
@Path("/{id}")
public Object provisioning(@PathParam String id) {
...
}
-}
+}
public class MyController {
+public class MyController {
@Path("/")
public Object provisioning(@QueryParam String q) {
...
}
-}
+}
public class MyController {
+public class MyController {
@Path("/")
@POST
public Object provisioning(@FormParam String username) {
...
}
-}
+}
public class MyController {
+public class MyController {
@Path("/")
@POST
public Object provisioning(MyObject body) {
...
}
-}
+}
public class Controller {
+public class Controller {
@GET("/{foo}")
public String bind(@BindParam MyBean bean) {
return "with custom mapping: " + bean;
}
-}
+}
public record MyBean(String value) {
+public record MyBean(String value) {
public static MyBean of(Context ctx) {
// build MyBean from HTTP request
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@FlashParam String success) { (1)
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@SessionParam String userId) { (1)
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(Session session) { (1)
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@ContextParam String userId) { (1)
...
}
-}
+}
public class MyController {
+public class MyController {
@GET
public Object provisioning(@ContextParam Map<String, Object> attributes) { (1)
...
}
-}
+}
public class FooController {
+public class FooController {
@GET("/{foo}")
public String multipleSources(@Param({ QUERY, PATH }) String foo) {
return "foo is: " + foo;
}
-}
+}
public class App extends Jooby {
+public class App extends Jooby {
{
mvc(new MyController());
}
@@ -5811,15 +5828,15 @@ (1)
}
-}
+}
public class App extends Jooby {
+public class App extends Jooby {
{
dispatch(() -> {
mvc(new MyBlockingController()); (1)
@@ -5847,9 +5864,9 @@
+}
public class MyController {
+public class MyController {
@GET("/nonblocking")
public String nonblocking() { (1)
return "I'm nonblocking";
@@ -5887,9 +5904,9 @@ (2)
return "I'm blocking";
}
-}
+}
public class MyController {
+public class MyController {
@GET("/blocking")
@Dispatch("single") (1)
public String blocking() {
return "I'm blocking";
}
-}
+}
{
+{
executor("single", Executors.newSingleThreadExecutor());
mvc(new MyController());
-}
+}
import javax.ws.rs.GET;
+import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("/jaxrs")
@@ -5996,9 +6013,9 @@
+}
false
Set the Route.mvcMethod when true.
jooby.routerPrefix
string
Prefix for generated class
jooby.routerSuffix
string
_
Suffix for generated class
{
+{
assets("/static/*"); (1)
-}
+}
{
+{
assets("/static/*", Paths.get("www")); (1)
-}
+}
{
+{
assets("/myfile.js", "/static/myfile.js");
-}
+}
{
+{
Path docs = Paths.get("docs"); (1)
assets("/docs/?*", docs); (2)
-}
+}
{
+{
AssetSource docs = AssetSource.create(Paths.get("docs")); (1)
assets("/docs/?*", new AssetHandler("index.html", docs)); (2)
-}
+}
{
+{
assets("/static/*", Paths.get("www"))
.setLastModified(false)
.setEtag(false);
-}
+}
{
+{
assets("/static/*", Paths.get("www"))
.setMaxAge(Duration.ofDays(365))
-}
+}
{
+{
assets("/static/*", Paths.get("www"))
.cacheControl(path -> {
if (path.endsWith("dont-cache-me.html")) {
@@ -6374,9 +6403,9 @@
+}
{
+{
assets("/static/*", Paths.get("www"))
.notFound(ctx -> {
throw new MyAssetException();
@@ -6407,9 +6436,9 @@
+}
{
+{
install(new MyTemplateEngineModule()); (1)
get("/", ctx -> {
MyModel model = ...; (2)
return new ModelAndView("index.html", model); (3)
});
-}
+}
{
+{
install(new MyTemplateEngineModule());
get("/", ctx -> {
@@ -6483,9 +6512,9 @@ (1)
});
-}
+}
{
+{
install(new HandlebarsModule()); (1)
install(new FreemarkerModule()); (2)
@@ -6555,9 +6584,9 @@ (4)
});
-}
+}
{
+{
get("/", ctx -> {
Session session = ctx.session(); (1)
@@ -6681,9 +6710,9 @@ (3)
});
-}
+}
{
+{
setSessionStore(SessionStore.memory(new Cookie("SESSION"))); (1)
get("/", ctx -> {
@@ -6727,9 +6756,9 @@
+}
{
+{
setSessionStore(SessionStore.memory(SessionToken.header("TOKEN"))); (1)
get("/", ctx -> {
@@ -6767,9 +6796,9 @@
+}
{
+{
setSessionStore(SessionStore.memory(SessionToken.comibe(SessionToken.cookie("SESSION"), SessionToken.header("TOKEN")))); (1)
get("/", ctx -> {
@@ -6807,9 +6836,9 @@
+}
{
+{
String secret = "super secret key"; (1)
setSessionStore(SessionStore.signed(secret)); (2)
@@ -6865,9 +6894,9 @@
+}
{
+{
String secret = "super secret key"; (1)
setSessionStore(SessionStore.signed(secret, SessionToken.header("TOKEN"))); (2)
@@ -6913,9 +6942,9 @@
+}
{
+{
ws("/ws", (ctx, configurer) -> { (1)
configurer.onConnect(ws -> {
ws.send("Connected"); (2)
@@ -6980,9 +7009,9 @@
+}
{
+{
ws("/ws/{key}", (ctx, configurer) -> {
String key = ctx.path("key").value(); (1)
String foo = ctx.session().get("foo").value(); (2)
...
});
-}
+}
import io.jooby.jackson.JacksonModule;
+import io.jooby.jackson.JacksonModule;
{
install(new JackonModule()); (1)
@@ -7085,9 +7114,9 @@ (3)
})
});
-}
+}
import io.jooby.jackson.JacksonModule;
+import io.jooby.jackson.JacksonModule;
{
install(new JackonModule()); (1)
@@ -7135,9 +7164,9 @@
+}
websocket.idleTimeout = 1h
+websocket.idleTimeout = 1h
websocket.maxSize = 128K
+websocket.maxSize = 128K
{
+{
sse("/sse", sse -> { (1)
sse.send("Welcome"); (2)
});
-}
+}
{
+{
sse("/sse", sse -> {
sse.onClose(() -> {
// clean up
});
});
-}
+}
{
+{
sse("/sse", sse -> {
sse.keepAlive(15, TimeUnit.SECONDS)
});
-}
+}
import static io.jooby.ExecutionMode.EVENT_LOOP;
+import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -7376,16 +7405,16 @@
+}
import static io.jooby.ExecutionMode.EVENT_LOOP;
+import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -7428,9 +7457,9 @@
+}
import static io.jooby.ExecutionMode.EVENT_LOOP;
+import static io.jooby.ExecutionMode.EVENT_LOOP;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -7489,9 +7518,9 @@
+}
import static io.jooby.ExecutionMode.WORKER;
+import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -7541,9 +7570,9 @@
+}
import static io.jooby.ExecutionMode.WORKER;
+import static io.jooby.ExecutionMode.WORKER;
import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -7583,9 +7612,9 @@
+}
import static io.jooby.Jooby.runApp;
+import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -7672,9 +7701,9 @@
+}
{
+{
configureServer(server -> {
server.workerThreads(Number);
});
-}
+}
{
+{
get("/json", ctx -> {
ctx.setContentType(MediaType.json);
return "{\"message\": \"Hello Raw Response\"}";
});
-}
+}
{
+{
get("/chunk", ctx -> {
try(Writer writer = ctx.responseWriter()) { (1)
writer.write("chunk1"); (2)
@@ -7894,9 +7923,9 @@ (3)
});
-}
+}
{
+{
get("/chunk", ctx -> {
return ctx.responseWriter(writer -> { (1)
writer.write("chunk1"); (2)
@@ -7940,9 +7969,9 @@
+}
{
+{
get("/download-file", ctx -> {
Path source = Paths.get("logo.png");
return new AttachedFile(source); (1)
@@ -7975,9 +8004,9 @@ (2)
});
-}
+}
FileDownload.Builder produceDownload(Context ctx) {
+FileDownload.Builder produceDownload(Context ctx) {
return FileDownload.build(...);
}
@@ -8018,9 +8047,9 @@
+}
{
+{
mode(EVENT_LOOP); (1)
use(ReactiveSupport.concurrent()); (2)
@@ -8063,9 +8092,9 @@ <
... (4)
});
})
-}
+}
{
+{
mode(WORKER); (1)
use(ReactiveSupport.concurrent()); (2)
@@ -8119,9 +8148,9 @@ <
... (4)
});
})
-}
+}
{
+{
mode(DEFAULT); (1)
use(ReactiveSupport.concurrent()); (2)
@@ -8175,9 +8204,9 @@ <
... (4)
});
})
-}
+}
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-mutiny</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
import io.jooby.mutiny;
+import io.jooby.mutiny;
import io.smallrye.mutiny.Uni;
{
@@ -8300,9 +8329,9 @@ Uni
.completionStage(supplyAsync(() -> "Uni"))
.map(it -> "Hello " + it);
})
-}
+}
import io.jooby.mutiny;
+import io.jooby.mutiny;
import io.smallrye.mutiny.Multi;
{
@@ -8335,9 +8364,9 @@
+}
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-rxjava3</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
import io.jooby.rxjava3.Reactivex;
+import io.jooby.rxjava3.Reactivex;
{
use(Reactivex.rx());
@@ -8415,9 +8444,9 @@
+}
import io.jooby.rxjava3.Reactivex;
+import io.jooby.rxjava3.Reactivex;
{
use(Reactivex.rx());
@@ -8446,9 +8475,9 @@
+}
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-reactor</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
import io.jooby.Reactor;
+import io.jooby.Reactor;
{
use(Reactor.reactor());
@@ -8527,9 +8556,9 @@ Mo
.fromCallable(() -> "Mono")
.map(it -> "Hello " + it);
})
-}
+}
import io.jooby.Reactor;
+import io.jooby.Reactor;
{
use(Reactor.reactor())
@@ -8558,9 +8587,9 @@ Fl
return Flux.range(1, 10)
.map(it -> it + ", ");
})
-}
+}
{
+{
coroutine {
get("/") {
delay(100) (1)
"Hello Coroutines!" (2)
}
}
-}
+}
{
+{
coroutine {
get("/") {
ctx.doSomething() (1)
@@ -8643,7 +8672,7 @@ (2)
return "Hello Coroutines!" (3)
-}
+}
{
+{
worker(Executors.newCachedThreadPool())
coroutine {
@@ -8683,7 +8712,7 @@ (3)
}
}
-}
+}
{
+{
coroutine(CoroutineStart.UNDISPATCHED) {
get("/") {
val n = 5 * 5 (1)
@@ -8720,7 +8749,7 @@ (3)
}
}
-}
+}
{
+{
get("/", ctx -> {
return ctx.send("Hello World!");
});
-}
+}
{
+{
errorCode(MyException.class, StatusCode.XXX);
-}
+}
{
+{
error((ctx, cause, statusCode) -> { (1)
Router router = ctx.getRouter();
router.getLog().error("found `{}` error", statusCode.value(), cause); (2)
ctx.setResponseCode(statusCode);
ctx.send("found `" + statusCode.value() + "` error"); (3)
});
-}
+}
import static io.jooby.MediaType.json;
+import static io.jooby.MediaType.json;
import static io.jooby.MediaType.html;
{
@@ -8993,9 +9022,9 @@ (5)
}
});
-}
+}
import static io.jooby.StatusCode.NOT_FOUND;
+import static io.jooby.StatusCode.NOT_FOUND;
{
error(NOT_FOUND, (ctx, cause, statusCode) -> {
ctx.send(statusCode); (1)
});
-}
+}
{
+{
error(MyException.class, (ctx, cause, statusCode) -> {
// log and process MyException
});
-}
+}
Most APIs have a way to report problems and errors, helping the user understand when something went wrong and what the issue is. +The method used depends on the API’s style, technology, and design. +Handling error reporting is an important part of the overall API design process.
+You could create your own error-reporting system, but that takes time and effort, both for the designer and for users who need to learn the custom approach. +Thankfully, there’s a standard called IETF RFC 7807 (later refined in RFC 9457) that can help.
+By adopting RFC 7807
, API designers don’t have to spend time creating a custom solution, and users benefit by recognizing a familiar format across different APIs.
+If it suits the API’s needs, using this standard benefits both designers and users alike.
Jooby
provides built-in support for Problem Details
.
To enable the ProblemDetails
, simply add the following line to your configuration:
problem.details.enabled = true
+This is the bare minimal configuration you need. +It enables a global error handler that catches all exceptions, transforms them into Problem Details compliant format and renders the response based on the Accept header value. It also sets the appropriate content-type in response (e.g. application/problem+json, application/problem+xml)
+All supported settings include:
+problem.details {
+ enabled = true
+ log4xxErrors = true (1)
+ muteCodes = [401, 403] (2)
+ muteTypes = ["com.example.MyMutedException"] (3)
+}
+1 | +By default, only server errors (5xx) will be logged. You can optionally enable the logging of client errors (4xx). If DEBUG logging level is enabled, the log will contain a stacktrace as well. |
+
2 | +You can optionally mute some status codes completely. | +
3 | +You can optionally mute some exceptions logging completely. | +
HttpProblem
class represents the RFC 7807
model. It is the main entity you need to work with to produce the problem.
There are several handy static methods to produce a simple HttpProblem
:
HttpProblem.valueOf(StatusCode status)
- will pick the title by status code.
+Don’t overuse it, the problem should have meaningful title
and detail
when possible.
HttpProblem.valueOf(StatusCode status, String title)
- with custom title
HttpProblem.valueOf(StatusCode status, String title, String detail)
- with title
and detail
HttpProblem
extends RuntimeException
so you can naturally throw it (as you do with exceptions):
import io.jooby.problem.HttpProblem;
+
+get("/users/{userId}", ctx -> {
+ var userId = ctx.path("userId").value();
+ User user = userRepository.findUser(userId);
+
+ if (user == null) {
+ throw HttpProblem.valueOf(StatusCode.NOT_FOUND,
+ "User Not Found",
+ "User with ID %s was not found in the system.".formatted(userId)
+ );
+ }
+ ...
+});
+Resulting response:
+{
+ "timestamp": "2024-10-05T14:10:41.648933100Z",
+ "type": "about:blank",
+ "title": "User Not Found",
+ "status": 404,
+ "detail": "User with ID 123 was not found in the system.",
+ "instance": null
+}
+Use builder to create a rich problem instance with all properties:
+throw HttpProblem.builder()
+ .type(URI.create("http://example.com/invalid-params"))
+ .title("Invalid input parameters")
+ .status(StatusCode.UNPROCESSABLE_ENTITY)
+ .detail("'Name' may not be empty")
+ .instance(URI.create("http://example.com/invalid-params/3325"))
+ .build();
+RFC 7807
has a simple extension model: APIs are free to add any other properties to the problem details object, so all properties other than the five ones listed above are extensions.
However, variadic root level fields are usually not very convenient for (de)serialization (especially in statically typed languages). That’s why HttpProblem
implementation grabs all extensions under a single root field parameters
. You can add parameters using builder like this:
throw HttpProblem.builder()
+ .title("Order not found")
+ .status(StatusCode.NOT_FOUND)
+ .detail("Order with ID $orderId could not be processed because it is missing or invalid.")
+ .param("reason", "Order ID format incorrect or order does not exist.")
+ .param("suggestion", "Please check the order ID and try again")
+ .param("supportReference", "/support")
+ .build();
+Resulting response:
+{
+ "timestamp": "2024-10-06T07:34:06.643235500Z",
+ "type": "about:blank",
+ "title": "Order not found",
+ "status": 404,
+ "detail": "Order with ID $orderId could not be processed because it is missing or invalid.",
+ "instance": null,
+ "parameters": {
+ "reason": "Order ID format incorrect or order does not exist.",
+ "suggestion": "Please check the order ID and try again",
+ "supportReference": "/support"
+ }
+}
+Some HTTP
codes (like 413
or 426
) require additional response headers, or it may be required by third-party system/integration. HttpProblem
support additional headers in response:
throw HttpProblem.builder()
+ .title("Invalid input parameters")
+ .status(StatusCode.UNPROCESSABLE_ENTITY)
+ .header("my-string-header", "string")
+ .header("my-int-header", 100)
+ .build();
+RFC 9457
finally described how errors should be delivered in HTTP APIs.
+It is basically another extension errors
on a root level. Adding errors is straight-forward using error()
or errors()
for bulk addition in builder:
throw HttpProblem.builder()
+ ...
+ .error(new HttpProblem.Error("First name cannot be blank", "/firstName"))
+ .error(new HttpProblem.Error("Last name is required", "/lastName"))
+ .build();
+In response:
+{
+ ...
+ "errors": [
+ {
+ "detail": "First name cannot be blank",
+ "pointer": "/firstName"
+ },
+ {
+ "detail": "Last name is required",
+ "pointer": "/lastName"
+ }
+ ]
+}
++ + | +
+
+
+If you need to enrich errors with more information feel free to extend |
+
Exception
to HttpProblem
Apparently, you may already have many custom Exception
classes in the codebase, and you want to make them Problem Details
compliant without complete re-write. You can achieve this by implementing HttpProblemMappable
interface. It allows you to control how exceptions should be transformed into HttpProblem
if default behaviour doesn’t suite your needs:
import io.jooby.problem.HttpProblemMappable;
+
+public class MyException implements HttpProblemMappable {
+
+ public HttpProblem toHttpProblem() {
+ return HttpProblem.builder()
+ ...
+ build();
+ }
+
+}
+Extending HttpProblem
and utilizing builder functionality makes it really easy:
public class OutOfStockProblem extends HttpProblem {
+
+ private static final URI TYPE = URI.create("https://example.org/out-of-stock");
+
+ public OutOfStockProblem(final String product) {
+ super(builder()
+ .type(TYPE)
+ .title("Out of Stock")
+ .status(StatusCode.BAD_REQUEST)
+ .detail(String.format("'%s' is no longer available", product))
+ .param("suggestions", List.of("Coffee Grinder MX-17", "Coffee Grinder MX-25"))
+ );
+ }
+}
+All the features described above should give you ability to rely solely on built-in global error handler. But, in case you still need custom exception handler for some reason, you still can do it:
+{
+ ...
+ error(MyCustomException.class, (ctx, cause, code) -> {
+ MyCustomException ex = (MyCustomException) cause;
+
+ HttpProblem problem = ... ; (1)
+
+ ctx.getRouter().getErrorHandler().apply(ctx, problem, code); (2)
+ });
+}
+1 | +Transform exception to HttpProblem |
+
2 | +Propagate the problem to ProblemDetailsHandler . It will handle the rest. |
+
+ + | +
+
+
+Do not attempt to render |
+
This section describes some built-in handler provided by Jooby.
The AccessLogHandler logs incoming requests using the NCSA format (a.k.a common log format).
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected @@ -9247,7 +9640,7 @@
cors {
+cors {
origin: "*"
credentials: true
methods: [GET, POST],
headers: [Content-Type],
maxAge: 30m
exposedHeaders: [Custom-Header]
-}
+}
The Cross Site Request Forgery Handler helps to protect from (CSRF) attacks. Cross-site request forgeries are a type of malicious exploit whereby unauthorized commands @@ -9423,10 +9816,10 @@
<form method="POST" action="...">
+<form method="POST" action="...">
<input name="csrf" value="{{csrf}}" type="hidden" />
...
-</form>
+</form>
The GracefulShutdown extension waits for existing requests to finish.
import io.jooby.Jooby;
+import io.jooby.Jooby;
import io.jooby.GracefulShutdown;
...
{
@@ -9484,15 +9877,15 @@ (1)
// other routes go here
-}
+}
Jooby doesn’t support HTTP HEAD
requests by default. To support them you have two options:
import io.jooby.Jooby;
+import io.jooby.Jooby;
import io.jooby.HeadHandler;
...
{
@@ -9552,9 +9945,9 @@
+}
Rate limit handler using Bucket4j.
<dependency>
+<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.0.1</version>
-</dependency>
+</dependency>
{
+{
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
Bucket bucket = Bucket4j.builder().addLimit(limit).build(); (1)
before(new RateLimitHandler(bucket)); (2)
-}
+}
{
+{
before(new RateLimitHandler(remoteAddress -> {
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
return Bucket4j.builder().addLimit(limit).build();
}));
-}
+}
{
+{
before(new RateLimitHandler(key -> {
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
return Bucket4j.builder().addLimit(limit).build();
}, "ApiKey"));
-}
+}
{
+{
Bandwidth limit = Bandwidth.simple(10, Duration.ofMinutes(1));
Bucket bucket = Bucket4j.builder().addLimit(limit).build(); (1)
before(new RateLimitHandler(bucket)); (2)
-}
+}
{
+{
ProxyManager<String> buckets = ...;
before(RateLimitHandler.cluster(key -> {
return buckets.getProxy(key, () -> {
return ...;
});
}));
-}
+}
The SSLHandler forces client to use HTTPS by redirecting non-HTTPS calls to the HTTPS version.
Jooby doesn’t support HTTP Trace
requests by default. To support them you have two options:
import io.jooby.Jooby;
+import io.jooby.Jooby;
import io.jooby.TraceHandler;
...
{
@@ -9868,9 +10261,9 @@ <
get("/", ctx -> {
...
});
-}
+}
Application configuration is based on config library. Configuration @@ -9908,7 +10301,7 @@
The application environment is available via the Environment class, which allows specifying one or many unique environment names.
@@ -9922,13 +10315,13 @@{
+{
Environment env = getEnvironment();
-}
+}
The default environment is available via loadEnvironment(EnvironmentOptions) method.
└── conf
+└── conf
└── application.conf
-└── myapp.jar
+└── myapp.jar
Environment env = getEnvironment();
+ Environment env = getEnvironment();
└── myapp.jar
- └── application.conf (file inside jar)
+└── myapp.jar
+ └── application.conf (file inside jar)
Property overrides is done in multiple ways (first-listed are higher priority):
foo = foo
+foo = foo
{
+{
Environment env = getEnvironment(); (1)
Config conf = env.getConfig(); (2)
System.out.println(conf.getString("foo")); (3)
-}
+}
java -jar myapp.jar foo=argument
+java -jar myapp.jar foo=argument
java -Dfoo=sysprop -jar myapp.jar
+java -Dfoo=sysprop -jar myapp.jar
foo=envar java -jar myapp.jar
+foo=envar java -jar myapp.jar
└── application.conf
-└── application.prod.conf
+└── application.conf
+└── application.prod.conf
foo = foo
-bar = devbar
+foo = foo
+bar = devbar
bar = prodbar
+bar = prodbar
Custom configuration and environment are available too using:
{
+{
Environment env = setEnvironmentOptions(new EnvOptions() (1)
.setFilename("myapp.conf")
)
-}
+}
{
+{
Config conf = ConfigFatory.load("/path/to/myapp.conf"); (1)
Environment env = new Env(customConfig, "prod"); (2)
setEnvironment(env); (3)
-}
+}
Jooby uses Slf4j for logging which give you some flexibility for choosing the logging framework.
The Logback is probably the first alternative for Slf4j due its natively implements the SLF4J API. Follow the next steps to use @@ -10316,19 +10709,19 @@
<dependency>
+<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
- <version>1.5.8</version>
-</dependency>
+ <version>1.5.10</version>
+</dependency>
The Log4j2 project is another good alternative for logging. Follow the next steps to use logback in your project:
@@ -10360,27 +10753,27 @@<dependency>
+<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
- <version>2.24.0</version>
+ <version>2.24.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
- <version>2.24.0</version>
-</dependency>
+ <version>2.24.1</version>
+</dependency>
Logging is integrated with the environment names. So it is possible to have a file name:
conf
+conf
└── logback.conf
-└── logback.prod.conf
+└── logback.prod.conf
public class App extends Jooby {
+public class App extends Jooby {
private static final Logger log = ...
public static void main(String[] args) {
runApp(args, App::new);
}
-}
+}
These are the application properties that Jooby uses:
This section will show you how to run unit and integration tests with Jooby.
1) Add Jooby test dependency:
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
public class App extends Jooby {
+public class App extends Jooby {
{
get("/", ctx -> "Easy unit testing!");
}
-}
+}
public class App extends Jooby {
+public class App extends Jooby {
{
get("/", ctx -> ctx
.setResponseCode(StatusCode.OK)
.send("Easy unit testing")
);
}
-}
+}
public class App extends Jooby {
+public class App extends Jooby {
{
post("/", ctx -> {
String name = ctx.form("name").value();
return name;
});
}
-}
+}
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class TestApp {
@@ -10758,9 +11151,9 @@
+}
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
public class App extends Jooby {
+public class App extends Jooby {
{
get("/", ctx -> "Easy testing!");
}
-}
+}
import io.jooby.JoobyTest;
+import io.jooby.JoobyTest;
@JoobyTest(App.class)
public class TestApp {
@@ -10875,9 +11268,9 @@
+}
@JoobyTest(App.class)
+@JoobyTest(App.class)
public void test(String serverPath) { (1)
-}
+}
Application arguments are supported using a factory method
strategy:
public class App extends Jooby {
+public class App extends Jooby {
public App(String argument) { (1)
get("/", ctx -> "Easy testing!");
}
-}
+}
import io.jooby.JoobyTest;
+import io.jooby.JoobyTest;
public class TestApp {
@@ -11034,9 +11427,9 @@ (2)
return new App("Argument"); (3)
}
-}
+}
The jooby run
tool allows to restart your application on code changes without exiting the JVM.
jooby run
is available as Maven and Gradle plugins.
<plugins>
+<plugins>
...
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
- <version>3.4.1</version>
+ <version>3.4.2</version>
</plugin>
...
-</plugins>
+</plugins>
<properties>
+<properties>
<application.class>myapp.App</application.class>
-</properties>
+</properties>
mvn jooby:run
+mvn jooby:run
Changing a java
or kt
file triggers a compilation request. Compilation is executed by
Maven/Gradle using an incremental build process.
The next example shows all the available options with their default values:
<plugins>
+<plugins>
...
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
- <version>3.4.1</version>
+ <version>3.4.2</version>
<configuration>
<mainClass>${application.class}</mainClass> (1)
<restartExtensions>conf,properties,class</restartExtensions> (2)
@@ -11202,17 +11595,17 @@ <
</configuration>
</plugin>
...
-</plugins>
+</plugins>
This section describes some packaging and distribution options.
Stork is packaging, launch and deploy tool for Java apps.
There are three server implementations:
@@ -11482,13 +11875,13 @@<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-jetty</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-undertow</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
Servers are automatically loaded using ServiceLoader
API. If you need to add it manually:
import io.jooby.netty.NettyServer;
+import io.jooby.netty.NettyServer;
{
install(new NettyServer());
-}
+}
Server options are available via ServerOptions class:
{
+{
setServerOptions(new ServerOptions()
.setBufferSize(16384)
.setCompressionLevel(6)
@@ -11627,9 +12020,9 @@
+}
Jooby supports HTTPS out of the box. By default HTTPS is disabled and all requests are served using HTTP. Jooby supports two certificate formats:
@@ -11755,17 +12148,17 @@{
+{
setServerOptions(new ServerOptions()
.setSecurePort(8443) (1)
);
-}
+}
mkcert -pkcs12 localhost
+mkcert -pkcs12 localhost
mkcert localhost
+mkcert localhost
To use a valid X.509 certificate, for example one created with Let’s Encrypt. You will need the .crt
and .key
files:
{
+{
SslOptions ssl = SslOptions.x509("path/to/server.crt", "path/to/server.key");
setServerOptions(new ServerOptions()
.setSsl(ssl) (1)
);
-}
+}
server {
+server {
ssl {
type: X509,
cert: "path/to/server.crt",
key: "path/to/server.key"
}
-}
+}
{
+{
setServerOptions(new ServerOptions()
.setSsl(SslOptions.from(getConfig()))
);
-}
+}
To use a valid PKCS12 certificate:
{
+{
SslOptions ssl = SslOptions.pkcs12("path/to/server.p12", "password");
setServerOptions(new ServerOptions()
.setSsl(ssl) (1)
);
-}
+}
server {
+server {
ssl {
type: PKCS12,
cert: "path/to/server.p12",
password: "password"
}
-}
+}
{
+{
setServerOptions(new ServerOptions()
.setSsl(SslOptions.from(getConfig()))
);
-}
+}
To enable 2-way TLS (Mutual TLS), set the trust certificate and client authentication. Setting the trust certificate is required if using self-signed or custom generated certificates so that the server will trust the client’s certificate signing authority.
{
+{
SslOptions ssl = SslOptions.pkcs12("path/to/server.p12", "password")
.setTrustCert(Files.newInputStream("path/to/trustCert")) (1)
.setTrustPassword("password") (2)
@@ -11972,16 +12365,16 @@
+}
{
+{
setServerOptions(new ServerOptions()
.setSsl(SslOptions.from(getConfig()))
);
-}
+}
Default protocol is TLSv1.3, TLSv1.2
. To override, just do:
{
+{
setServerOptions(new ServerOptions()
.setSsl(new SslOptions().setProtocol("TLSv1.3", "TLSv1.2"))
);
-}
+}
SSL support is provided using built-in JDK capabilities. Jooby offers an OpenSSL support using Conscrypt.
@@ -12110,13 +12503,13 @@<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-conscrypt</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
HTTP2 support is provided across web server implementation. You need to enabled http2
option
programmatically or via application.conf
properties.
Jooby comes with a simple extension mechanism. The Extension API allows to configure @@ -12203,7 +12596,7 @@
We are going to develop a custom extension that configure a DataSource
service.
import io.jooby.Extension;
+import io.jooby.Extension;
public class MyExtension implements Extension {
@@ -12223,9 +12616,9 @@ (4)
}
-}
+}
1) Add Avaje Inject to your project
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-avaje-inject</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
public class App extends Jooby {
+public class App extends Jooby {
{
install(AvajeInjectModule.of()); (1)
@@ -12395,9 +12788,9 @@
+}
Configuration properties can be injected using the @Named
annotation. As Avaje checks beans at compile time, @External
is required to prevent false-positive compilation errors:
currency = USD
+currency = USD
Avaje Inject will also provisioning MVC routes
public class App extends Jooby {
+public class App extends Jooby {
{
install(AvajeInjectModule.of()); (1)
@@ -12473,15 +12866,15 @@
+}
1) Add Dagger to your project
<dependency>
+<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>2.20</version>
-</dependency>
+</dependency>
<build>
+<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -12546,15 +12939,15 @@
+</build>
import static io.jooby.Jooby.runApp;
+import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -12582,9 +12975,9 @@
+}
Integration of MVC routes with Dagger is as simple as:
import static io.jooby.Jooby.runApp;
+import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -12650,9 +13043,9 @@
+}
import static io.jooby.Jooby.runApp;
+import static io.jooby.Jooby.runApp;
public class App extends Jooby {
@@ -12700,9 +13093,9 @@
+}
1) Add Guice dependency to your project:
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-guice</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>
import io.jooby.guice.GuiceModule;
+import io.jooby.guice.GuiceModule;
import io.jooby.kt.runApp;
public class App extends Jooby {
@@ -12770,9 +13163,9 @@
+}
Configuration properties can be injected using the @Named
annotation:
currency = USD
+currency = USD
Guice will also provisioning MVC routes
import io.jooby.guice.GuiceModule;
+import io.jooby.guice.GuiceModule;
import io.jooby.kt.runApp
public class App extends Jooby {
@@ -12855,9 +13248,9 @@
+}
Modules are a key concept for building reusable and configurable pieces of software.
@@ -12920,7 +13313,7 @@You will find here notes/tips about how to migrate from 2.x to 3.x.
Jooby is now compatible with Java Module system.
Kotlin was removed from core, you need to the jooby-kotlin
dependency:
<dependency>
+<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-kotlin</artifactId>
- <version>3.4.1</version>
-</dependency>
+ <version>3.4.2</version>
+</dependency>