mn create-app example.micronaut.micronautguide \
--features=properties,eclipsestore,serialization-jackson,validation \
--build=gradle \
--lang=java \
--test=junit
|
Note
|
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool. If you don’t specify the --lang argument, Java is used as the language. If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.
|
The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.
If you use Micronaut Launch, select Micronaut Application as application type and add properties, eclipsestore, serialization-jackson, and validation features.
|
Note
|
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application.
|
Dependencies
The eclipsestore feature adds the following dependencies:
annotationProcessor("io.micronaut.eclipsestore:micronaut-eclipsestore-processor")
compileOnly("io.micronaut.eclipsestore:micronaut-eclipsestore-processor")
implementation("io.micronaut.eclipsestore:micronaut-eclipsestore")
Domain object
Create a Fruit class which will be used as the domain object.
java/src/main/java/example/micronaut/Fruit.java
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
@Serdeable //
public class Fruit {
@NonNull
@NotBlank //
private final String name;
@Nullable //
private String description;
public Fruit(@NonNull String name,
@Nullable String description) {
this.name = name;
this.description = description;
}
@NonNull
public String getName() {
return name;
}
@Nullable
public String getDescription() {
return description;
}
public void setDescription(@Nullable String description) {
this.description = description;
}
}
Root Object
Create a FruitContainer POJO which we will be used as the root of our object graph.
java/src/main/java/example/micronaut/FruitContainer.java
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class FruitContainer {
@NonNull
private final Map<String, Fruit> fruits = new ConcurrentHashMap<>();
@NonNull
public Map<String, Fruit> getFruits() {
return fruits;
}
}
Configuration
Add the following snippet to application.properties to configure EclipseStore.
src/main/resources/application.properties
eclipsestore.storage.main.root-class=example.micronaut.FruitContainer
eclipsestore.storage.main.storage-directory=build/fruit-storage
Command object
And a FruitCommand class which will be used as the command object over HTTP.
java/src/main/java/example/micronaut/FruitCommand.java
package example.micronaut;
import io.micronaut.core.annotation.Creator;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotBlank;
@Serdeable //
public class FruitCommand {
@NonNull
@NotBlank //
private final String name;
@Nullable //
private final String description;
public FruitCommand(@NonNull String name) {
this(name, null);
}
@Creator
public FruitCommand(@NonNull String name,
@Nullable String description) {
this.name = name;
this.description = description;
}
@NonNull
public String getName() {
return name;
}
@Nullable
public String getDescription() {
return description;
}
}
Repository
Create a repository interface to encapsulate the CRUD actions for Fruit.
java/src/main/java/example/micronaut/FruitRepository.java
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Collection;
import jakarta.validation.Valid;
interface FruitRepository {
@NonNull
Collection<Fruit> list();
@NonNull
Fruit create(@NonNull @NotNull @Valid FruitCommand fruit) //
throws FruitDuplicateException;
@Nullable
Fruit update(@NonNull @NotNull @Valid FruitCommand fruit); //
@Nullable
Fruit find(@NonNull @NotBlank String name);
void delete(@NonNull @NotNull @Valid FruitCommand fruit); //
}
Error handling
In the event an attempt is made to create a duplicate fruit, we will catch the exception with a custom class.
java/src/main/java/example/micronaut/FruitDuplicateException.java
package example.micronaut;
public class FruitDuplicateException extends RuntimeException{
public FruitDuplicateException(String name) {
super("Fruit '" + name + "' already exists.");
}
}
A custom ExceptionHandler handles this exception and returns a 400 error with a sensible message.
java/src/main/java/example/micronaut/FruitDuplicateExceptionHandler.java
package example.micronaut;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import io.micronaut.http.server.exceptions.response.ErrorContext;
import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor;
import jakarta.inject.Singleton;
@Produces //
@Singleton //
public class FruitDuplicateExceptionHandler implements ExceptionHandler<FruitDuplicateException, HttpResponse<?>> {
private final ErrorResponseProcessor<?> errorResponseProcessor;
public FruitDuplicateExceptionHandler(ErrorResponseProcessor<?> errorResponseProcessor) {
this.errorResponseProcessor = errorResponseProcessor;
}
@Override
public HttpResponse<?> handle(HttpRequest request, FruitDuplicateException exception) {
ErrorContext errorContext = ErrorContext.builder(request)
.cause(exception)
.errorMessage(exception.getMessage())
.build();
return errorResponseProcessor.processResponse(errorContext, HttpResponse.unprocessableEntity());
}
}
Repository implementation
Implement the FruitRepository interface.
When an object in your graph changes, you need to persist the object that contains the change.
This can be achieved through the StoreParams and StoreReturn annotations.
java/src/main/java/example/micronaut/FruitRepositoryImpl.java
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.eclipsestore.RootProvider;
import io.micronaut.eclipsestore.annotations.StoreParams;
import io.micronaut.eclipsestore.annotations.StoreReturn;
import jakarta.inject.Singleton;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import java.util.Collection;
import java.util.Map;
@Singleton //
public class FruitRepositoryImpl implements FruitRepository {
private final RootProvider<FruitContainer> rootProvider;
FruitRepositoryImpl(RootProvider<FruitContainer> rootProvider) { //
this.rootProvider = rootProvider;
}
@Override
@NonNull
public Collection<Fruit> list() {
return rootProvider.root().getFruits().values(); //
}
@Override
@NonNull
public Fruit create(@NonNull @NotNull @Valid FruitCommand fruit) throws FruitDuplicateException {
Map<String, Fruit> fruits = rootProvider.root().getFruits();
if (fruits.containsKey(fruit.getName())) {
throw new FruitDuplicateException(fruit.getName());
}
return performCreate(fruits, fruit);
}
@StoreParams("fruits") //
protected Fruit performCreate(Map<String, Fruit> fruits, FruitCommand fruit) {
Fruit newFruit = new Fruit(fruit.getName(), fruit.getDescription());
fruits.put(fruit.getName(), newFruit);
return newFruit;
}
@Nullable
public Fruit update(@NonNull @NotNull @Valid FruitCommand fruit) {
Map<String, Fruit> fruits = rootProvider.root().getFruits();
Fruit foundFruit = fruits.get(fruit.getName());
if (foundFruit != null) {
return performUpdate(foundFruit, fruit);
}
return null;
}
@StoreReturn //
protected Fruit performUpdate(@NonNull Fruit foundFruit, @NonNull FruitCommand fruit) {
foundFruit.setDescription(fruit.getDescription());
return foundFruit;
}
@Override
@Nullable
public Fruit find(@NonNull @NotBlank String name) {
return rootProvider.root().getFruits().get(name);
}
@Override
public void delete(@NonNull @NotNull @Valid FruitCommand fruit) {
performDelete(fruit);
}
@StoreReturn //
protected Map<String, Fruit> performDelete(FruitCommand fruit) {
if (rootProvider.root().getFruits().remove(fruit.getName()) != null) {
return rootProvider.root().getFruits();
}
return null;
}
}
Controller
java/src/main/java/example/micronaut/FruitController.java
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import io.micronaut.http.annotation.Status;
import io.micronaut.scheduling.annotation.ExecuteOn;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.Collection;
import static io.micronaut.scheduling.TaskExecutors.BLOCKING;
@Controller("/fruits") //
class FruitController {
private final FruitRepository fruitRepository;
FruitController(FruitRepository fruitRepository) { //
this.fruitRepository = fruitRepository;
}
@Get //
Collection<Fruit> list() {
return fruitRepository.list();
}
@ExecuteOn(BLOCKING)
@Post //
@Status(HttpStatus.CREATED) //
Fruit create(@NonNull @NotNull @Valid @Body FruitCommand fruit) { //
return fruitRepository.create(fruit);
}
@ExecuteOn(BLOCKING)
@Put
Fruit update(@NonNull @NotNull @Valid @Body FruitCommand fruit) {
return fruitRepository.update(fruit);
}
@Get("/{name}") //
Fruit find(@PathVariable String name) {
return fruitRepository.find(name);
}
@ExecuteOn(BLOCKING)
@Delete
@Status(HttpStatus.NO_CONTENT)
void delete(@NonNull @Valid @Body FruitCommand fruit) {
fruitRepository.delete(fruit);
}
}
Test
Create a test that verifies the validation of the FruitCommand POJO when we invoke the FruitRepository interface:
java/src/test/java/example/micronaut/FruitRepositoryTest.java
package example.micronaut;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.function.Executable;
import jakarta.validation.ConstraintViolationException;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest(startApplication = false) //
class FruitRepositoryTest {
@Inject
FruitRepository fruitRepository;
@Test
void methodsValidateParamers() {
Executable e = () -> fruitRepository.create(new FruitCommand(""));
assertThrows(ConstraintViolationException.class, e);
}
}
Create a test that verifies the validation of the FruitCommand POJO when we create a new entity via POST:
java/src/test/java/example/micronaut/FruitValidationControllerTest.java
package example.micronaut;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest //
class FruitValidationControllerTest {
@Inject
@Client("/")
HttpClient httpClient; //
@Test
void fruitIsValidated() {
HttpClientResponseException exception = assertThrows(
HttpClientResponseException.class,
() -> httpClient.toBlocking().exchange(HttpRequest.POST("/fruits", new FruitCommand("", "")))
);
assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus());
}
}
We will use temporary directories to persist our data under test.
To facilitate this, create a base test class that handles the creation of a temporary folder, and configuring the application.
java/src/test/java/example/micronaut/BaseTest.java
package example.micronaut;
import io.micronaut.test.support.TestPropertyProvider;
import org.junit.jupiter.api.io.TempDir;
import jakarta.validation.constraints.NotNull;
import java.io.File;
import java.util.Collections;
import java.util.Map;
import io.micronaut.core.annotation.NonNull;
abstract class BaseTest implements TestPropertyProvider {
@TempDir
static File tempDir;
@Override
@NonNull
public Map<String, String> getProperties() {
return Collections.singletonMap(
"microstream.storage.main.storage-directory", tempDir.getAbsolutePath()
);
}
}
Create a test that validates FruitDuplicateExceptionHandler.
java/src/test/java/example/micronaut/FruitDuplicationExceptionHandlerTest.java
package example.micronaut;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest //
@TestInstance(TestInstance.Lifecycle.PER_CLASS) //
class FruitDuplicationExceptionHandlerTest extends BaseTest {
@Inject
@Client("/")
HttpClient httpClient; //
@Test
void duplicatedFruitsReturns400() {
FruitCommand banana = new FruitCommand("Banana");
HttpRequest<?> request = HttpRequest.POST("/fruits", banana);
HttpResponse<?> response = httpClient.toBlocking().exchange(request);
assertEquals(HttpStatus.CREATED, response.status());
HttpClientResponseException exception = assertThrows(
HttpClientResponseException.class,
() -> httpClient.toBlocking().exchange(request)
);
assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, exception.getStatus());
HttpRequest<?> deleteRequest = HttpRequest.DELETE("/fruits", banana);
HttpResponse<?> deleteResponse = httpClient.toBlocking().exchange(deleteRequest);
assertEquals(HttpStatus.NO_CONTENT, deleteResponse.status());
}
}
java/src/test/java/example/micronaut/FruitClient.java
package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import io.micronaut.http.client.annotation.Client;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Optional;
@Client("/fruits")
interface FruitClient {
@Get
Iterable<Fruit> list();
@Get("/{name}")
Optional<Fruit> find(@NonNull @NotBlank @PathVariable String name);
@Post
HttpResponse<Fruit> create(@NonNull @NotNull @Valid @Body FruitCommand fruit);
@Put
Optional<Fruit> update(@NonNull @NotNull @Valid @Body FruitCommand fruit);
@NonNull
@Delete
HttpStatus delete(@NonNull @Valid @Body FruitCommand fruit);
}
And finally, create a test that checks the controller works against EclipseStore correctly:
java/src/test/java/example/micronaut/FruitControllerTest.java
package example.micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static org.junit.jupiter.api.Assertions.*;
class FruitControllerTest extends BaseTest {
@Test
void testInteractionWithTheController() {
FruitCommand apple = new FruitCommand("apple", "Keeps the doctor away");
String bananaName = "banana";
String bananaDescription = "Yellow and curved";
Map<String, Object> properties = new HashMap<>(getProperties());
try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) { //
FruitClient fruitClient = embeddedServer.getApplicationContext().getBean(FruitClient.class);
HttpResponse<Fruit> response = fruitClient.create(new FruitCommand(bananaName));
assertEquals(HttpStatus.CREATED, response.getStatus());
assertTrue(response.getBody().isPresent());
Fruit banana = response.getBody().get();
List<Fruit> fruitList = fruitsList(fruitClient);
assertEquals(1, fruitList.size());
assertEquals(banana.getName(), fruitList.get(0).getName());
assertNull(fruitList.get(0).getDescription());
Optional<Fruit> bananaOptional = fruitClient.update(apple);
assertFalse(bananaOptional.isPresent());
response = fruitClient.create(apple);
assertEquals(HttpStatus.CREATED, response.getStatus());
assertTrue(fruitsStream(fruitClient)
.anyMatch(f -> "Keeps the doctor away".equals(f.getDescription())));
bananaOptional = fruitClient.update(new FruitCommand(bananaName, bananaDescription));
assertTrue(bananaOptional.isPresent());
assertEquals(
Stream.of("Keeps the doctor away", "Yellow and curved").collect(Collectors.toSet()),
fruitsStream(fruitClient)
.map(Fruit::getDescription)
.collect(Collectors.toSet())
);
}
try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) { //
FruitClient fruitClient = embeddedServer.getApplicationContext().getBean(FruitClient.class);
assertEquals(2, numberOfFruits(fruitClient));
fruitClient.delete(apple);
fruitClient.delete(new FruitCommand(bananaName, bananaDescription));
}
try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) { //
FruitClient fruitClient = embeddedServer.getApplicationContext().getBean(FruitClient.class);
assertEquals(0, numberOfFruits(fruitClient));
}
}
private int numberOfFruits(FruitClient fruitClient) {
return fruitsList(fruitClient).size();
}
private List<Fruit> fruitsList(FruitClient fruitClient) {
return fruitsStream(fruitClient)
.collect(Collectors.toList());
}
private Stream<Fruit> fruitsStream(FruitClient fruitClient) {
Iterable<Fruit> fruits = fruitClient.list();
return StreamSupport.stream(fruits.spliterator(), false);
}
}