This document is the manual for the workshop Micronaut with GraalVM in Practice. The final source code of the demo application is also in this repository. The most effective way to learn is not to access the source code in this repository, but to work through this workshop step by step.
The demo application provides a service that is controlled by HTTP requests. The command-line tool cURL, which is available on many systems and known by many developers, is used for this purpose. A user-friendly alternative is HTTPie. In the following, this document contains the calls for both variants.
mn create-app --features=http-server,http-client,graal-native-image ch.jug.micronaut.beers.beers
cd beers
./gradlew run
curl -v http://localhost:8080
http localhost:8080
package ch.jug.micronaut.beers;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
public class Beer {
private final Long id;
private final String name;
private final String brewery;
@JsonCreator
public Beer(@JsonProperty("id") final Long id,
@JsonProperty("name") final String name,
@JsonProperty("brewery") final String brewery) {
this.id = id;
this.name = name;
this.brewery = brewery;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getBrewery() {
return brewery;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((brewery == null) ? 0 : brewery.hashCode());
result = prime * result + ((id == null) ? 0 : id.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Beer other = (Beer) obj;
if (brewery == null) {
if (other.brewery != null)
return false;
} else if (!brewery.equals(other.brewery))
return false;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
return "Beer [id=" + id + ", name=" + name + ", brewery=" + brewery + "]";
}
}
package ch.jug.micronaut.beers;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.inject.Singleton;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class BeerService {
private static final Logger logger = LoggerFactory.getLogger(BeerService.class);
private final List<Beer> beers = new CopyOnWriteArrayList<>();
public BeerService() {
addBeer(new Beer(1L, "Luzerner Bier", "Brauerei Luzern AG"));
addBeer(new Beer(2L, "Lozärner Bier", "Lozärner Bier AG"));
addBeer(new Beer(3L, "Urbräu", "Tavolago AG"));
}
public Flowable<Beer> getAllBeers() {
return Flowable.create(emitter -> {
for (final Beer beer : beers) {
if (emitter.isCancelled())
return;
Thread.sleep(1_000);
logger.info("Emitting beer: {}", beer);
emitter.onNext(beer);
}
emitter.onComplete();
}, BackpressureStrategy.BUFFER);
}
public void addBeer(final Beer beer) {
beers.add(beer);
}
}
In the service class, we artificially slow down the response to calling the beers to simulate a long-running operation and thus better see the effect of asynchronous communication in the log.
package ch.jug.micronaut.beers;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.reactivex.Flowable;
@Controller("/beers")
public class BeerController {
private final BeerService service;
public BeerController(final BeerService service) {
this.service = service;
}
@Get
public Flowable<Beer> getAllBeers() {
return service.getAllBeers();
}
@Post
public void addBeer(@Body final Beer beer) {
service.addBeer(beer);
}
}
Let’s start and check, what we created so far:
./gradlew run
curl -v -N http://localhost:8080/beers
http localhost:8080/beers
curl -v -H "Content-Type: application/json" -d '{"id": 4, "name": "DukeDrop", "brewery": "Duke’s Brewery"}' http://localhost:8080/beers
http POST localhost:8080/beers id=4 name=DukeDrop brewery="Duke’s Brewery"
curl -v -N http://localhost:8080/beers
http localhost:8080/beers
package ch.jug.micronaut.beers;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;
@Client("/beers")
public interface BeerClient {
@Get
public Flowable<Beer> fetchBeers();
}
package ch.jug.micronaut.beers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.micronaut.scheduling.annotation.Scheduled;
public class BeerFetcher {
private static final Logger logger = LoggerFactory.getLogger(BeerFetcher.class);
private final BeerClient client;
public BeerFetcher(final BeerClient client) {
this.client = client;
}
@Scheduled(fixedDelay = "5s", initialDelay = "10s")
public void fetchSomeBeer() {
client.fetchBeers()
.doOnError(e -> logger.error("Can't fetch beers!", e))
.forEach(beer -> logger.info("Receiving beer: {}", beer));
}
}
./gradlew run
Warning
|
You have a problem. You decide to solve it with configuration. Now you have <%= $problems %> problems. (Daniel Terhorst-North) |
beers:
url: http://localhost:8080/beers
initial-delay: 10s
fixed-delay: 5s
@Client("${beers.url}")
@Scheduled(fixedDelay = "${beers.fixed-delay}", initialDelay = "${beers.initial-delay}")
./gradlew run
beers:
url: http://localhost:8080/beers
initial-delay: 10s
fixed-delay: ${BEERS_FIXED_DELAY}
BEERS_FIXED_DELAY=2s ./gradlew run
Try to start our service without specifying the environment variable:
./gradlew run
If an environment variable is mentioned in the configuration file, it is getting mandatory! You can use environment variables directly without mentioning in the configuration file. If they are not set, the values default to null
, 0
or false
, depending on the type.
@Scheduled(fixedDelay = "${beers.fixed-delay:5s}", initialDelay = "${beers.initial-delay:10s}")
@Client("${beers.url:`http://localhost:8080/beers`}")
./gradlew run
Play around with commenting the beers:
section from the configuration file in and out in combination with and without an environment variable to check out the different behavior.
First, create an own event class for added beers:
package ch.jug.micronaut.beers;
public class BeerAddedEvent {
private final Beer beer;
public BeerAddedEvent(final Beer beer) {
this.beer = beer;
}
public Beer getBeer() {
return beer;
}
}
Now, lets publish the event for each beer added:
private ApplicationEventPublisher eventPublisher;
public BeerService(final ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
addBeer(new Beer(1L, "Luzerner Bier", "Brauerei Luzern AG"));
addBeer(new Beer(2L, "Lozärner Bier", "Lozärner Bier AG"));
addBeer(new Beer(3L, "Urbräu", "Tavolago AG"));
}
public void addBeer(final Beer beer) {
beers.add(beer);
eventPublisher.publishEvent(new BeerAddedEvent(beer));
}
package ch.jug.micronaut.beers;
import io.micronaut.runtime.event.annotation.EventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BeerAddedListener {
private static final Logger logger = LoggerFactory.getLogger(BeerAddedEvent.class);
@EventListener
public void doSomethingOnNewBeer(final BeerAddedEvent event) {
logger.info("Wow, there is a new beer available: {}", event.getBeer());
}
}
@CircuitBreaker(delay = "5s", attempts = "5", multiplier = "2", reset = "10m")
package ch.jug.micronaut.beers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.micronaut.retry.annotation.Fallback;
import io.reactivex.Flowable;
@Fallback
public class NoBeer implements BeerClient {
private static final Logger logger = LoggerFactory.getLogger(NoBeer.class);
@Override
public Flowable<Beer> fetchBeers() {
logger.info("Fallback implementation called!");
return Flowable.empty();
}
}
package ch.jug.micronaut.beers;
import org.junit.jupiter.api.Test;
import io.micronaut.test.annotation.MicronautTest;
import javax.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.ArrayList;
import java.util.List;
@MicronautTest
class BeerServiceTest {
@Inject
BeerService service;
@Test
public void testBeers() throws Exception {
final List<Beer> beers = new ArrayList<>();
service.getAllBeers().subscribe(beers::add);
assertEquals(3, beers.size());
}
}
./gradlew test
package ch.jug.micronaut.beers;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.runtime.server.EmbeddedServer;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@MicronautTest
public class BeerControllerTest {
@Inject
private EmbeddedServer server;
@Inject
@Client("/")
private RxHttpClient client;
@Test
public void testBody() throws Exception {
final HttpRequest<String> request = HttpRequest.GET("/beers");
final String body = client.toBlocking().retrieve(request);
assertNotNull(body);
assertEquals(body, "[{\"id\":1,\"name\":\"Luzerner Bier\",\"brewery\":\"Brauerei Luzern AG\"},{\"id\":2,\"name\":\"Lozärner Bier\",\"brewery\":\"Lozärner Bier AG\"},{\"id\":3,\"name\":\"Urbräu\",\"brewery\":\"Tavolago AG\"}]");
}
@Test
public void testStatus() throws Exception {
try(RxHttpClient client = server.getApplicationContext().createBean(RxHttpClient.class, server.getURL())) {
assertEquals(HttpStatus.OK, client.toBlocking().exchange("/beers").status());
}
}
}
./gradlew test
Play around and modify the expectations to check out assertion errors.
./gradlew assemble
native-image --no-server -jar build/libs/beers-0.1-all.jar
./beers
BEERS_FIXED_DELAY=3s ./beers
curl -v -N http://localhost:8080/beers
http localhost:8080/beers
curl -v -H "Content-Type: application/json" -d '{"id": 4, "name": "DukeDrop", "brewery": "Duke’s Brewery"}' http://localhost:8080/beers
http POST localhost:8080/beers id=4 name=DukeDrop brewery="Duke’s Brewery"
curl -v -N http://localhost:8080/beers
http localhost:8080/beers
docker build . -t beers
docker run -p 8080:8080 beers
curl -v -N http://localhost:8080/beers
http localhost:8080/beers
curl -v -H "Content-Type: application/json" -d '{"id": 4, "name": "DukeDrop", "brewery": "Duke’s Brewery"}' http://localhost:8080/beers
http POST localhost:8080/beers id=4 name=DukeDrop brewery="Duke’s Brewery"
curl -v -N http://localhost:8080/beers
http localhost:8080/beers