|
| 1 | += Agents and Tools |
| 2 | + |
| 3 | +Quarkus provides a novel reactive API called Mutiny, with the goal of easing the development of highly scalable, resilient, and asynchronous systems. |
| 4 | + |
| 5 | +In this chapter we're going to see some examples of how Mutiny changes the design of our Quarkus applications. |
| 6 | +to online beer database (https://punkapi.com/documentation/v2) to retrieve beer information. |
| 7 | +This API does not return all beers at once, so we'll need to navigate through the pages to fetch all the information. |
| 8 | +Then we're going to filter all the beers with an ABV greater than 15.0 and return all these beers in a Reactive REST endpoint. |
| 9 | + |
| 10 | +== Add the Mutiny extension |
| 11 | + |
| 12 | +Create a new Quarkus project, for example using https://code.quarkus.io/ website. |
| 13 | + |
| 14 | +Then open a new terminal window, and make sure you’re at the root of your `{project-name}` project, then run: |
| 15 | + |
| 16 | +[tabs] |
| 17 | +==== |
| 18 | +Maven:: |
| 19 | ++ |
| 20 | +-- |
| 21 | +[.console-input] |
| 22 | +[source,bash,subs="+macros,+attributes"] |
| 23 | +---- |
| 24 | +./mvnw quarkus:add-extension -Dextension=quarkus-mutiny,quarkus-rest-client-reactive-jsonb,quarkus-resteasy-reactive-jsonb |
| 25 | +---- |
| 26 | +
|
| 27 | +-- |
| 28 | +Quarkus CLI:: |
| 29 | ++ |
| 30 | +-- |
| 31 | +[.console-input] |
| 32 | +[source,bash,subs="+macros,+attributes"] |
| 33 | +---- |
| 34 | +quarkus extension add quarkus-mutiny,quarkus-rest-client-reactive-jsonb,quarkus-resteasy-reactive-jsonb |
| 35 | +---- |
| 36 | +-- |
| 37 | +==== |
| 38 | + |
| 39 | +== Create Beer POJO |
| 40 | + |
| 41 | +Create a new `Beer` Java class in `src/main/java` in the `org.acme` package with the following contents: |
| 42 | + |
| 43 | +[.console-input] |
| 44 | +[source,java] |
| 45 | +---- |
| 46 | +package org.acme; |
| 47 | +
|
| 48 | +import jakarta.json.bind.annotation.JsonbCreator; |
| 49 | +
|
| 50 | +public class Beer { |
| 51 | +
|
| 52 | + private String name; |
| 53 | + private String tagline; |
| 54 | + private double abv; |
| 55 | +
|
| 56 | + private Beer(String name, String tagline, double abv) { |
| 57 | + this.name = name; |
| 58 | + this.tagline = tagline; |
| 59 | + this.abv = abv; |
| 60 | + } |
| 61 | +
|
| 62 | + @JsonbCreator |
| 63 | + public static Beer of(String name, String tagline, double abv) { |
| 64 | + return new Beer(name, tagline, abv); |
| 65 | + } |
| 66 | +
|
| 67 | + public String getName() { |
| 68 | + return name; |
| 69 | + } |
| 70 | +
|
| 71 | + public String getTagline() { |
| 72 | + return tagline; |
| 73 | + } |
| 74 | +
|
| 75 | + public double getAbv() { |
| 76 | + return abv; |
| 77 | + } |
| 78 | +
|
| 79 | +} |
| 80 | +---- |
| 81 | + |
| 82 | +== Create BeerService |
| 83 | + |
| 84 | +Now we're going to implement a Java interface that mimics the remote REST endpoint. |
| 85 | + |
| 86 | +Create a new `BeerService` Java interface in `src/main/java` in the `org.acme` package with the following contents: |
| 87 | + |
| 88 | +[.console-input] |
| 89 | +[source,java] |
| 90 | +---- |
| 91 | +package org.acme; |
| 92 | +
|
| 93 | +import java.util.List; |
| 94 | +
|
| 95 | +import jakarta.json.JsonArray; |
| 96 | +import jakarta.ws.rs.GET; |
| 97 | +import jakarta.ws.rs.Path; |
| 98 | +import jakarta.ws.rs.PathParam; |
| 99 | +import jakarta.ws.rs.Produces; |
| 100 | +import jakarta.ws.rs.QueryParam; |
| 101 | +import jakarta.ws.rs.core.MediaType; |
| 102 | +
|
| 103 | +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; |
| 104 | +
|
| 105 | +import io.smallrye.mutiny.Uni; |
| 106 | +
|
| 107 | +@Path("/v2") |
| 108 | +@RegisterRestClient |
| 109 | +public interface BeerService { |
| 110 | +
|
| 111 | + @GET |
| 112 | + @Path("/beers") |
| 113 | + @Produces(MediaType.APPLICATION_JSON) |
| 114 | + Uni<List<Beer>> getBeers(@QueryParam("page") int page); |
| 115 | +} |
| 116 | +---- |
| 117 | + |
| 118 | +== Configure REST Client properties |
| 119 | + |
| 120 | +Add the following properties to your `application.properties` in `src/main/resources`: |
| 121 | + |
| 122 | +[.console-input] |
| 123 | +[source,properties] |
| 124 | +---- |
| 125 | +org.acme.BeerService/mp-rest/url=https://api.punkapi.com |
| 126 | +---- |
| 127 | + |
| 128 | +== Pagination + Filtering |
| 129 | + |
| 130 | +We want to query all the beers page by page and filter by its _abv_ value. |
| 131 | + |
| 132 | +image::pagination.png[] |
| 133 | + |
| 134 | +=== Create BeerResource |
| 135 | + |
| 136 | +Create a new `BeerResource` Java class in `src/main/java` in the `org.acme` package with the following contents: |
| 137 | + |
| 138 | +[.console-input] |
| 139 | +[source,java] |
| 140 | +---- |
| 141 | +package org.acme; |
| 142 | +
|
| 143 | +import java.util.List; |
| 144 | +import java.util.concurrent.atomic.AtomicInteger; |
| 145 | +
|
| 146 | +import jakarta.json.Json; |
| 147 | +import jakarta.json.JsonArray; |
| 148 | +import jakarta.json.JsonMergePatch; |
| 149 | +import jakarta.json.JsonObject; |
| 150 | +import jakarta.json.JsonValue; |
| 151 | +import jakarta.ws.rs.GET; |
| 152 | +import jakarta.ws.rs.Path; |
| 153 | +import jakarta.ws.rs.PathParam; |
| 154 | +
|
| 155 | +import org.eclipse.microprofile.rest.client.inject.RestClient; |
| 156 | +import io.smallrye.mutiny.Multi; |
| 157 | +import io.smallrye.mutiny.Uni; |
| 158 | +
|
| 159 | +@Path("/beer") |
| 160 | +public class BeerResource { |
| 161 | +
|
| 162 | + @RestClient |
| 163 | + BeerService beerService; |
| 164 | +
|
| 165 | + @GET |
| 166 | + public Multi<Beer> beers() { |
| 167 | + return Multi.createBy().repeating() // <1> |
| 168 | + .uni( |
| 169 | + () -> new AtomicInteger(1), |
| 170 | + i -> beerService.getBeers(i.getAndIncrement()) // <2> |
| 171 | + ) |
| 172 | + .until(List::isEmpty) // <3> |
| 173 | + .onItem().<Beer>disjoint() // <4> |
| 174 | + .select().where(b -> b.getAbv() > 15.0); // <5> |
| 175 | + } |
| 176 | +} |
| 177 | +---- |
| 178 | +<1> Creates a `Multi`. |
| 179 | +<2> The supplier will start with `1` and will query the remote endpoint asking for page `i`. |
| 180 | +<3> The multi will end when the beer list returned is empty. |
| 181 | +<4> We dismember all the returned lists and create a sequence of beers. |
| 182 | +<5> And then we filter the `Multi` with beers with `ABV > 15.0`. |
| 183 | + |
| 184 | +=== Invoke the endpoint |
| 185 | + |
| 186 | +You can check your new implementation by pointing your browser to http://localhost:8080/beer[window=_blank] |
| 187 | + |
| 188 | +You can also run the following command: |
| 189 | + |
| 190 | +[.console-input] |
| 191 | +[source,bash] |
| 192 | +---- |
| 193 | +curl localhost:8080/beer |
| 194 | +---- |
| 195 | + |
| 196 | +[.console-output] |
| 197 | +[source,json] |
| 198 | +---- |
| 199 | +[ |
| 200 | + { |
| 201 | + "abv": 55, |
| 202 | + "name": "The End Of History", |
| 203 | + "tagline": "The World's Strongest Beer." |
| 204 | + }, |
| 205 | + { |
| 206 | + "abv": 16.5, |
| 207 | + "name": "Anarchist Alchemist", |
| 208 | + "tagline": "Triple Hopped Triple Ipa." |
| 209 | + }, |
| 210 | + { |
| 211 | + "abv": 15.2, |
| 212 | + "name": "Lumberjack Stout", |
| 213 | + "tagline": "Blueberry Bacon Stout." |
| 214 | + }, |
| 215 | + { |
| 216 | + "abv": 18.3, |
| 217 | + "name": "Bowman's Beard - B-Sides", |
| 218 | + "tagline": "English Barley Wine." |
| 219 | + }, |
| 220 | + { |
| 221 | + "abv": 41, |
| 222 | + "name": "Sink The Bismarck!", |
| 223 | + "tagline": "IPA For The Dedicated." |
| 224 | + }, |
| 225 | + { |
| 226 | + "abv": 16.2, |
| 227 | + "name": "Tokyo*", |
| 228 | + "tagline": "Intergalactic Stout. Rich. Smoky. Fruity." |
| 229 | + }, |
| 230 | + { |
| 231 | + "abv": 18, |
| 232 | + "name": "AB:02", |
| 233 | + "tagline": "Triple Dry Hopped Imperial Red Ale." |
| 234 | + }, |
| 235 | + { |
| 236 | + "abv": 17.2, |
| 237 | + "name": "Black Tokyo Horizon (w/Nøgne Ø & Mikkeller)", |
| 238 | + "tagline": "Imperial Stout Collaboration." |
| 239 | + }, |
| 240 | + { |
| 241 | + "abv": 16.1, |
| 242 | + "name": "Dog D", |
| 243 | + "tagline": "Anniversary Imperial Stout." |
| 244 | + }, |
| 245 | + { |
| 246 | + "abv": 32, |
| 247 | + "name": "Tactical Nuclear Penguin", |
| 248 | + "tagline": "Uber Imperial Stout." |
| 249 | + }, |
| 250 | + { |
| 251 | + "abv": 16.1, |
| 252 | + "name": "Dog E", |
| 253 | + "tagline": "Ninth Anniversary Imperial Stout." |
| 254 | + }, |
| 255 | + { |
| 256 | + "abv": 17, |
| 257 | + "name": "Dog G", |
| 258 | + "tagline": "11th Anniversary Imperial Stout." |
| 259 | + } |
| 260 | +] |
| 261 | +---- |
| 262 | + |
| 263 | +== Parallel Calls |
| 264 | + |
| 265 | +Suppose that now, you want to query two beers by its id, (so execute two requests against the remote API), and then compare its _abv_ values. |
| 266 | + |
| 267 | +image::parallel.png[] |
| 268 | + |
| 269 | +=== Modify BeerService |
| 270 | + |
| 271 | +Open `BeerService` interface and add the following method to get a beer: |
| 272 | + |
| 273 | +[.console-input] |
| 274 | +[source,java] |
| 275 | +---- |
| 276 | +@GET |
| 277 | +@Path("/beers/{id}") |
| 278 | +@Produces(MediaType.APPLICATION_JSON) |
| 279 | +Uni<JsonArray> getBeer(@PathParam("id") int id); |
| 280 | +---- |
| 281 | + |
| 282 | +=== Modify BeerResource |
| 283 | + |
| 284 | +Open `BeerResource` class and add the following methods to do in parallel the both calls. |
| 285 | + |
| 286 | +[.console-input] |
| 287 | +[source,java] |
| 288 | +---- |
| 289 | +@GET |
| 290 | +@Path("/{beerA}/{beerB}") |
| 291 | +public Uni<JsonValue> compare(@PathParam("beerA") int beerA, @PathParam("beerB") int beerB) { |
| 292 | + Uni<JsonArray> beer1 = beerService.getBeer(beerA); // <1> |
| 293 | + Uni<JsonArray> beer2 = beerService.getBeer(beerB); // <2> |
| 294 | +
|
| 295 | + return Uni.combine() |
| 296 | + .all() |
| 297 | + .unis(beer1, beer2) // <3> |
| 298 | + .with((b1, b2) -> this.compare(b1, b2)); // <4> |
| 299 | +} |
| 300 | +
|
| 301 | +private JsonValue compare(JsonArray beerA, JsonArray beerB) { |
| 302 | + JsonObject source = beerA.get(0).asJsonObject(); |
| 303 | + JsonObject target = beerB.get(0).asJsonObject(); |
| 304 | +
|
| 305 | + String beerAName = source.getString("name"); |
| 306 | + String beerBName = target.getString("name"); |
| 307 | +
|
| 308 | + double beerAAbv = source.getJsonNumber("abv").doubleValue(); |
| 309 | + double beerBAbv = target.getJsonNumber("abv").doubleValue(); |
| 310 | +
|
| 311 | + return Json.createObjectBuilder() |
| 312 | + .add("source-name", beerAName) |
| 313 | + .add("target-name", beerBName) |
| 314 | + .add("source-abv", beerAAbv) |
| 315 | + .add("target-abv", beerBAbv) |
| 316 | + .build(); |
| 317 | +} |
| 318 | +---- |
| 319 | +<1> Executes request for first beer |
| 320 | +<2> Executes request for second beer |
| 321 | +<3> Waits until both requests returns a response |
| 322 | +<4> Compare both beers and returns an object with the result |
| 323 | + |
| 324 | +=== Invoke the endpoint |
| 325 | + |
| 326 | +You can check your new implementation by pointing your browser to http://localhost:8080/beer/1/2[window=_blank] |
| 327 | + |
| 328 | +You can also run the following command: |
| 329 | + |
| 330 | +[.console-input] |
| 331 | +[source,bash] |
| 332 | +---- |
| 333 | +curl localhost:8080/beer/1/2 |
| 334 | +---- |
| 335 | + |
| 336 | +[.console-output] |
| 337 | +[source,json] |
| 338 | +---- |
| 339 | +{"source-name":"Buzz","target-name":"Trashy Blonde","source-abv":4.5,"target-abv":4.1} |
| 340 | +---- |
0 commit comments