mn create-app example.micronaut.micronautguide --build=maven --lang=java
Table of Contents
Error Handling
Learn about error handling in the Micronaut framework.
Authors: Sergio del Amo
Micronaut Version: 3.9.2
1. Getting Started
In this guide, we will create a Micronaut application written in Java.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.8 or greater installed with
JAVA_HOME
configured appropriately
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.
|
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
4.1. Global @Error
We want to display a custom Not Found
page when the user attempts to access a URI that has no defined routes.
The views module provides support for view rendering on the server side and does so by rendering views on the I/O thread pool in order to avoid blocking the Netty event loop.
To use the view rendering features described in this section, add the following dependency on your classpath. Add the following dependency to your build file:
<dependency>
<groupId>io.micronaut.views</groupId>
<artifactId>micronaut-views-velocity</artifactId>
<scope>compile</scope>
</dependency>
The Micronaut framework ships out-of-the-box with support for Apache Velocity, Thymeleaf or Handlebars. In this guide, we use Apache Velocity.
Create a notFound.vm
view:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Not Found</title>
</head>
<body>
<h1>NOT FOUND</h1>
<p><b>The page you were looking for appears to have been moved, deleted or does not exist.</b></p>
<p>This is most likely due to:</p>
<ul>
<li>An outdated link on another site</li>
<li>A typo in the address / URL</li>
</ul>
</body>
</html>
Create a NotFoundController
:
package example.micronaut;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Error;
import io.micronaut.http.hateoas.JsonError;
import io.micronaut.http.hateoas.Link;
import io.micronaut.views.ViewsRenderer;
import java.util.Collections;
@Controller("/notfound") (1)
public class NotFoundController {
private final ViewsRenderer viewsRenderer;
public NotFoundController(ViewsRenderer viewsRenderer) { (2)
this.viewsRenderer = viewsRenderer;
}
@Error(status = HttpStatus.NOT_FOUND, global = true) (3)
public HttpResponse notFound(HttpRequest request) {
if (request.getHeaders()
.accept()
.stream()
.anyMatch(mediaType -> mediaType.getName().contains(MediaType.TEXT_HTML))) { (4)
return HttpResponse.ok(viewsRenderer.render("notFound", Collections.emptyMap(), request))
.contentType(MediaType.TEXT_HTML);
}
JsonError error = new JsonError("Page Not Found")
.link(Link.SELF, Link.of(request.getUri()));
return HttpResponse.<JsonError>notFound()
.body(error); (5)
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /notfound . |
2 | Inject an available ViewRenderer bean to render an HTML view. |
3 | The Error declares which HttpStatus error code to handle (in this case 404). We declare the method as a global error handler due to global = true . |
4 | If the request Accept HTTP Header contains text/html , we respond an HTML View. |
5 | By default, we respond JSON. |
4.2. Local @Error
Micronaut validation is built on the standard framework – JSR 380, also known as Bean Validation 2.0.
Hibernate Validator is a reference implementation of the validation API. Micronaut has built-in support for validation of beans that are annotated with javax.validation
annotations.
The necessary dependencies are included by default when creating a new application, so you don’t need to add anything else.
Then create a view to display a form:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Create Book</title>
<style type="text/css">
form fieldset li {
list-style-type: none;
}
#errors span { color: red; }
</style>
</head>
<body>
<h1>Create Book</h1>
<form action="/books/save" method="post">
<fieldset>
<ol>
<li>
<label for="title">Title</label>
<input type="text" id="title" name="title" value="$title"/>
</li>
<li>
<label for="pages">Pages</label>
<input type="text" id="pages" name="pages" value="$pages"/>
</li>
<li>
<input type="submit" value="Save"/>
</li>
</ol>
</fieldset>
</form>
#if( $errors )
<ul id="errors">
#foreach( $error in $errors )
<li><span>$error</span></li>
#end
</ul>
#end
</body>
</html>
To use the serialization features described in this section, add the following dependency to your build file:
<dependency>
<groupId>io.micronaut.serde</groupId>
<artifactId>micronaut-serde-jackson</artifactId>
<scope>compile</scope>
</dependency>
Create a controller to map the form submission:
package example.micronaut;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Error;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Produces;
import io.micronaut.views.View;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Controller("/books") (1)
public class BookController {
@View("bookscreate") (2)
@Get("/create") (3)
public Map<String, Object> create() {
return createModelWithBlankValues();
}
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) (4)
@Post("/save") (5)
public HttpResponse save(@Valid @Body CommandBookSave cmd) { (6)
return HttpResponse.ok();
}
private Map<String, Object> createModelWithBlankValues() {
final Map<String, Object> model = new HashMap<>();
model.put("title", "");
model.put("pages", "");
return model;
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /books . |
2 | Use @View annotation to indicate the view name which should be used to render a view for the route. |
3 | You can specify the HTTP verb that a controller action responds to. To respond to a GET request, use the io.micronaut.http.annotation.Get annotation. |
4 | @Consumes annotation takes a String[] of supported media types for an incoming request. |
5 | The @Post annotation maps the index method to all requests that use an HTTP POST |
6 | Add @Valid to any method parameter which requires validation. We use a POJO to encapsulate the form submission. |
Create the POJO encapsulating the submission:
package example.micronaut;
import io.micronaut.serde.annotation.Serdeable;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Positive;
@Serdeable (1)
public class CommandBookSave {
@NotBlank (2)
private String title;
@Positive (3)
private int pages;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
}
1 | Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized. |
2 | title is required and must be not blank. |
3 | pages must be greater than 0. |
When the form submission fails, we want to display the errors in the UI as the next image illustrates:
An easy way to achieve it is to capture the javax.validation.ConstraintViolationException
exception in a local @Error
handler. Modify BookController.java
:
...
class BookController {
...
..
private final MessageSource messageSource;
public BookController(MessageSource messageSource) { (1)
this.messageSource = messageSource;
}
...
.
@View("bookscreate")
@Error(exception = ConstraintViolationException.class) (2)
public Map<String, Object> onSavedFailed(HttpRequest request, ConstraintViolationException ex) { (3)
final Map<String, Object> model = createModelWithBlankValues();
model.put("errors", messageSource.violationsMessages(ex.getConstraintViolations()));
Optional<CommandBookSave> cmd = request.getBody(CommandBookSave.class);
cmd.ifPresent(bookSave -> populateModel(model, bookSave));
return model;
}
private void populateModel(Map<String, Object> model, CommandBookSave bookSave) {
model.put("title", bookSave.getTitle());
model.put("pages", bookSave.getPages());
}
private Map<String, Object> createModelWithBlankValues() {
final Map<String, Object> model = new HashMap<>();
model.put("title", "");
model.put("pages", "");
return model;
}
..
...
}
1 | Constructor injection |
2 | By default @Error annotations are local. We specify the exception which we want to handle. |
3 | You can access the original HttpRequest which triggered the exception. |
Create a jakarta.inject.Singleton
to encapsulate the generation of a list of messages from a Set
of ConstraintViolation
:
package example.micronaut;
import jakarta.inject.Singleton;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Singleton
public class MessageSource {
public List<String> violationsMessages(Set<ConstraintViolation<?>> violations) {
return violations.stream()
.map(MessageSource::violationMessage)
.collect(Collectors.toList());
}
private static String violationMessage(ConstraintViolation violation) {
StringBuilder sb = new StringBuilder();
Path.Node lastNode = lastNode(violation.getPropertyPath());
if (lastNode != null) {
sb.append(lastNode.getName());
sb.append(" ");
}
sb.append(violation.getMessage());
return sb.toString();
}
private static Path.Node lastNode(Path path) {
Path.Node lastNode = null;
for (final Path.Node node : path) {
lastNode = node;
}
return lastNode;
}
}
5. ExceptionHandler
Another mechanism to handle global exception is to use a ExceptionHandler
.
Modify the controller and add a method to throw an exception:
@Controller("/books") (1)
public class BookController {
...
..
.
@Produces(MediaType.TEXT_PLAIN)
@Get("/stock/{isbn}")
public Integer stock(String isbn) {
throw new OutOfStockException();
}
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /books . |
package example.micronaut;
public class OutOfStockException extends RuntimeException {
}
Implement a ExceptionHandler; a generic hook for handling exceptions that occurs during the execution of an HTTP request.
package example.micronaut;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;
@Produces
@Singleton (1)
@Requires(classes = {OutOfStockException.class, ExceptionHandler.class}) (2)
public class OutOfStockExceptionHandler implements ExceptionHandler<OutOfStockException, HttpResponse> { (3)
@Override
public HttpResponse handle(HttpRequest request, OutOfStockException exception) {
return HttpResponse.ok(0); (4)
}
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
2 | This bean loads if OutOfStockException , ExceptionHandler are available. |
3 | Specify the Throwable to handle. |
4 | Return 200 OK with a body of 0; no stock. |
6. Generate a Micronaut Application Native Executable with GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.
Compiling native executables ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
6.1. Native executable generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
To generate a native executable using Maven, run:
./mvnw package -Dpackaging=native-image
The native executable is created in the target
directory and can be run with target/micronautguide
.
After you run the native executable, execute a curl request:
curl -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' localhost:8080/foo
You should get successful response.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Not Found</title>
</head>
<body>
<h1>NOT FOUND</h1>
....
7. Next steps
Explore more features with Micronaut Guides.
8. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.