7.3 Declarative HTTP Clients with @Client

Now that you have an understanding of the workings of the lower-level HTTP client, let’s take a look at Micronaut’s support for declarative clients via the Client annotation.

Essentially, the @Client annotation can be declared on any interface or abstract class, and through the use of Introduction Advice the abstract methods are implemented for you at compile time, greatly simplifying the creation of HTTP clients.

Let’s start with a simple example. Given the following class:

Pet.java

  1. public class Pet {
  2. private String name;
  3. private int age;
  4. public String getName() {
  5. return name;
  6. }
  7. public void setName(String name) {
  8. this.name = name;
  9. }
  10. public int getAge() {
  11. return age;
  12. }
  13. public void setAge(int age) {
  14. this.age = age;
  15. }
  16. }

Pet.java

  1. class Pet {
  2. String name
  3. int age
  4. }

Pet.java

  1. class Pet {
  2. var name: String? = null
  3. var age: Int = 0
  4. }

You can define a common interface for saving new Pet instances:

PetOperations.java

  1. import io.micronaut.http.annotation.Post;
  2. import io.micronaut.validation.Validated;
  3. import org.reactivestreams.Publisher;
  4. import io.micronaut.core.async.annotation.SingleResult;
  5. import javax.validation.constraints.Min;
  6. import javax.validation.constraints.NotBlank;
  7. @Validated
  8. public interface PetOperations {
  9. @Post
  10. @SingleResult
  11. Publisher<Pet> save(@NotBlank String name, @Min(1L) int age);
  12. }

PetOperations.java

  1. import io.micronaut.http.annotation.Post
  2. import io.micronaut.validation.Validated
  3. import org.reactivestreams.Publisher
  4. import io.micronaut.core.async.annotation.SingleResult
  5. import javax.validation.constraints.Min
  6. import javax.validation.constraints.NotBlank
  7. @Validated
  8. interface PetOperations {
  9. @Post
  10. @SingleResult
  11. Publisher<Pet> save(@NotBlank String name, @Min(1L) int age)
  12. }

PetOperations.java

  1. import io.micronaut.http.annotation.Post
  2. import io.micronaut.validation.Validated
  3. import javax.validation.constraints.Min
  4. import javax.validation.constraints.NotBlank
  5. import io.micronaut.core.async.annotation.SingleResult
  6. import org.reactivestreams.Publisher
  7. @Validated
  8. interface PetOperations {
  9. @Post
  10. @SingleResult
  11. fun save(@NotBlank name: String, @Min(1L) age: Int): Publisher<Pet>
  12. }

Note how the interface uses Micronaut’s HTTP annotations which are usable on both the server and client side. You can also use javax.validation constraints to validate arguments.

Be aware that some annotations, such as Produces and Consumes, have different semantics between server and client side usage. For example, @Produces on a controller method (server side) indicates how the method’s return value is formatted, while @Produces on a client indicates how the method’s parameters are formatted when sent to the server. While this may seem a little confusing, it is logical considering the different semantics between a server producing/consuming vs a client: a server consumes an argument and returns a response to the client, whereas a client consumes an argument and sends output to a server.

Additionally, to use the javax.validation features, add the validation module to your build:

  1. implementation("io.micronaut:micronaut-validator")
  1. <dependency>
  2. <groupId>io.micronaut</groupId>
  3. <artifactId>micronaut-validator</artifactId>
  4. </dependency>

On the server-side of Micronaut you can implement the PetOperations interface:

PetController.java

  1. import io.micronaut.http.annotation.Controller;
  2. import org.reactivestreams.Publisher;
  3. import reactor.core.publisher.Mono;
  4. import io.micronaut.core.async.annotation.SingleResult;
  5. @Controller("/pets")
  6. public class PetController implements PetOperations {
  7. @Override
  8. @SingleResult
  9. public Publisher<Pet> save(String name, int age) {
  10. Pet pet = new Pet();
  11. pet.setName(name);
  12. pet.setAge(age);
  13. // save to database or something
  14. return Mono.just(pet);
  15. }
  16. }

PetController.java

  1. import io.micronaut.http.annotation.Controller
  2. import org.reactivestreams.Publisher
  3. import io.micronaut.core.async.annotation.SingleResult
  4. import reactor.core.publisher.Mono
  5. @Controller("/pets")
  6. class PetController implements PetOperations {
  7. @Override
  8. @SingleResult
  9. Publisher<Pet> save(String name, int age) {
  10. Pet pet = new Pet(name: name, age: age)
  11. // save to database or something
  12. return Mono.just(pet)
  13. }
  14. }

PetController.java

  1. import io.micronaut.http.annotation.Controller
  2. import reactor.core.publisher.Mono
  3. import io.micronaut.core.async.annotation.SingleResult
  4. import org.reactivestreams.Publisher
  5. @Controller("/pets")
  6. open class PetController : PetOperations {
  7. @SingleResult
  8. override fun save(name: String, age: Int): Publisher<Pet> {
  9. val pet = Pet()
  10. pet.name = name
  11. pet.age = age
  12. // save to database or something
  13. return Mono.just(pet)
  14. }
  15. }

You can then define a declarative client in src/test/java that uses @Client to automatically implement a client at compile time:

PetClient.java

  1. import io.micronaut.http.client.annotation.Client;
  2. import org.reactivestreams.Publisher;
  3. import io.micronaut.core.async.annotation.SingleResult;
  4. import javax.validation.constraints.Min;
  5. import javax.validation.constraints.NotBlank;
  6. @Client("/pets") (1)
  7. public interface PetClient extends PetOperations { (2)
  8. @Override
  9. @SingleResult
  10. Publisher<Pet> save(@NotBlank String name, @Min(1L) int age); (3)
  11. }

PetClient.java

  1. import io.micronaut.http.client.annotation.Client
  2. import org.reactivestreams.Publisher
  3. import io.micronaut.core.async.annotation.SingleResult
  4. @Client("/pets") (1)
  5. interface PetClient extends PetOperations { (2)
  6. @Override
  7. @SingleResult
  8. Publisher<Pet> save(String name, int age) (3)
  9. }

PetClient.java

  1. import io.micronaut.http.client.annotation.Client
  2. import io.micronaut.core.async.annotation.SingleResult
  3. import org.reactivestreams.Publisher
  4. @Client("/pets") (1)
  5. interface PetClient : PetOperations { (2)
  6. @SingleResult
  7. override fun save(name: String, age: Int): Publisher<Pet> (3)
  8. }
1The Client annotation is used with a value relative to the current server, in this case /pets
2The interface extends from PetOperations
3The save method is overridden. See the warning below.
Notice in the above example we override the save method. This is necessary if you compile without the -parameters option since Java does not retain parameters names in bytecode otherwise. Overriding is not necessary if you compile with -parameters. In addition, when overriding methods you should ensure any validation annotations are declared again since these are not Inherited annotations.

Once you have defined a client you can @Inject it wherever you need it.

Recall that the value of @Client can be:

  • An absolute URI, e.g. [https://api.twitter.com/1.1](https://api.twitter.com/1.1)

  • A relative URI, in which case the server targeted is the current server (useful for testing)

  • A service identifier. See the section on Service Discovery for more information on this topic.

In production you typically use a service ID and Service Discovery to discover services automatically.

Another important thing to notice regarding the save method in the example above is that it returns a Single type.

This is a non-blocking reactive type - typically you want your HTTP clients to not block. There are cases where you may want an HTTP client that does block (such as in unit tests), but this is rare.

The following table illustrates common return types usable with @Client:

Table 1. Micronaut Response Types
TypeDescriptionExample Signature

Publisher

Any type that implements the Publisher interface

Flux<String> hello()

HttpResponse

An HttpResponse and optional response body type

Mono<HttpResponse<String>> hello()

Publisher

A Publisher implementation that emits a POJO

Mono<Book> hello()

CompletableFuture

A Java CompletableFuture instance

CompletableFuture<String> hello()

CharSequence

A blocking native type. Such as String

String hello()

T

Any simple POJO type.

Book show()

Generally, any reactive type that can be converted to the Publisher interface is supported as a return type, including (but not limited to) the reactive types defined by RxJava 1.x, RxJava 2.x, and Reactor 3.x.

Returning CompletableFuture instances is also supported. Note that returning any other type results in a blocking request and is not recommended other than for testing.