7.2 Declarative HTTP Clients with @Client

Now that you have gathered an understanding of the workings of the lower level HTTP client, it is time to 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 will be 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 io.reactivex.Single;
  4. import javax.validation.constraints.Min;
  5. import javax.validation.constraints.NotBlank;
  6. @Validated
  7. public interface PetOperations {
  8. @Post
  9. Single<Pet> save(@NotBlank String name, @Min(1L) int age);
  10. }

PetOperations.java

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

PetOperations.java

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

Note how the interface uses Micronaut’s HTTP annotations which are usable on both the server and client side. Also, as you can see you can 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 at first, it is actually 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 you should have the validation and hibernate-validator dependencies on your classpath. For example in build.gradle:

build.gradle

  1. compile "io.micronaut:micronaut-validation"
  2. compile "io.micronaut.configuration:micronaut-hibernate-validator"

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

PetController.java

  1. import io.micronaut.http.annotation.Controller;
  2. import io.reactivex.Single;
  3. @Controller("/pets")
  4. public class PetController implements PetOperations {
  5. @Override
  6. public Single<Pet> save(String name, int age) {
  7. Pet pet = new Pet();
  8. pet.setName(name);
  9. pet.setAge(age);
  10. // save to database or something
  11. return Single.just(pet);
  12. }
  13. }

PetController.java

  1. import io.micronaut.http.annotation.Controller
  2. import io.reactivex.Single
  3. @Controller("/pets")
  4. class PetController implements PetOperations {
  5. @Override
  6. Single<Pet> save(String name, int age) {
  7. Pet pet = new Pet()
  8. pet.setName(name)
  9. pet.setAge(age)
  10. // save to database or something
  11. return Single.just(pet)
  12. }
  13. }

PetController.java

  1. import io.micronaut.http.annotation.Controller
  2. import io.reactivex.Single
  3. @Controller("/pets")
  4. open class PetController : PetOperations {
  5. override fun save(name: String, age: Int): Single<Pet> {
  6. val pet = Pet()
  7. pet.name = name
  8. pet.age = age
  9. // save to database or something
  10. return Single.just(pet)
  11. }
  12. }

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

PetClient.java

  1. import io.micronaut.http.client.annotation.Client;
  2. import io.reactivex.Single;
  3. @Client("/pets") (1)
  4. public interface PetClient extends PetOperations { (2)
  5. @Override
  6. Single<Pet> save(String name, int age); (3)
  7. }

PetClient.java

  1. import io.micronaut.http.client.annotation.Client
  2. import io.reactivex.Single
  3. @Client("/pets") (1)
  4. interface PetClient extends PetOperations { (2)
  5. @Override
  6. Single<Pet> save(String name, int age) (3)
  7. }

PetClient.java

  1. import io.micronaut.http.client.annotation.Client
  2. import io.reactivex.Single
  3. @Client("/pets") (1)
  4. interface PetClient : PetOperations { (2)
  5. override fun save(name: String, age: Int): Single<Pet> (3)
  6. }
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 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 the byte code otherwise. If you compile with -parameters then overriding is not necessary.

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

Recall that the value of @Client can be:

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

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

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

In a production deployment you would 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 is returns a Single type.

This is a non-blocking reactive type and typically you want your HTTP clients not to block. There are cases where you may want to write an HTTP client that does block (such as in unit test cases), but this are 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

Flowable<String> hello()

HttpResponse

An HttpResponse and optional response body type

Single<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 will result in a blocking request and is not recommended other than for testing.