7.3.1 Customizing Parameter Binding
The previous example presented a trivial example that uses the parameters of a method to represent the body of a POST
request:
PetOperations.java
@Post
Single<Pet> save(@NotBlank String name, @Min(1L) int age);
The save
method when called will perform an HTTP POST
with the following JSON by default:
Example Produced JSON
{"name":"Dino", "age":10}
You may however want to customize what is sent as the body, the parameters, URI variables and so on. The @Client annotation is very flexible in this regard and supports the same HTTP Annotations as Micronaut’s HTTP server.
For example, the following defines a URI template and the name
parameter is used as part of the URI template, whilst @Body is used to declare that the contents to send to the server are represented by the Pet
POJO:
PetOperations.java
@Post("/{name}")
Single<Pet> save(
@NotBlank String name, (1)
@Body @Valid Pet pet) (2)
1 | The name parameter, included as part of the URI, and declared @NotBlank |
2 | The pet parameter, used to encode the body and declared @Valid |
The following table summarizes the parameter annotations, their purpose, and provides an example:
Annotation | Description | Example |
---|---|---|
Allows to specify the parameter that is the body of the request |
| |
Allows specifying parameters that should be sent as cookies |
| |
Allows specifying parameters that should be sent as HTTP headers |
| |
Allows customizing the name of the URI parameter to bind from |
| |
Used to bind a parameter exclusively from a Path Variable. |
| |
Allows specifying parameters that should be set as request attributes |
|
Always use @Produces or @Consumes instead of supplying a header for Content-Type or Accept . |
Type Based Binding Parameters
Some parameters are recognized by their type instead of their annotation. The following table summarizes the parameter types, their purpose, and provides an example:
Type | Description | Example |
---|---|---|
Allows binding of basic authorization credentials |
|
Custom Binding
The ClientArgumentRequestBinder API is what is responsible for binding client arguments to the request. Custom binder classes registered as beans will automatically be used during the binding process. Annotation based binders are searched for first, with type based binders being searched if a binder was not found.
This is an experimental feature in 2.1 and subject to change! One limitation of binding is that binders may not manipulate or set the request URI because it can be a combination of many arguments. |
Binding By Annotation
To control how an argument is bound to the request based on the annotation applied to the argument, create a bean of type AnnotatedClientArgumentRequestBinder. Any custom annotations must be annotated with @Bindable.
In this example, see the following client:
Client With @Metadata Argument
@Client("/")
public interface MetadataClient {
@Get("/client/bind")
String get(@Metadata Map<String, Object> metadata);
}
Client With @Metadata Argument
@Client("/")
interface MetadataClient {
@Get("/client/bind")
String get(@Metadata Map metadata)
}
Client With @Metadata Argument
@Client("/")
interface MetadataClient {
@Get("/client/bind")
operator fun get(@Metadata metadata: Map<String, Any>): String
}
The argument is annotated with the following annotation:
@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Bindable
public @interface Metadata {
}
@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable
import java.lang.annotation.*
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Bindable
@interface Metadata {
}
@Metadata Annotation
import io.micronaut.core.bind.annotation.Bindable
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
@Bindable
annotation class Metadata
Without any additional code, the client will attempt to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect so a custom binder must be created.
The following binder will handle arguments passed to clients that are annotated with the @Metadata
annotation and then mutate the request to contain the desired headers. The implementation could be modified to accept more types of data other than Map
.
Annotation Argument Binder
import edu.umd.cs.findbugs.annotations.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder;
import io.micronaut.http.client.bind.ClientRequestUriContext;
import org.jetbrains.annotations.NotNull;
import javax.inject.Singleton;
import java.util.Map;
@Singleton
public class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {
@NotNull
@Override
public Class<Metadata> getAnnotationType() {
return Metadata.class;
}
@Override
public void bind(@NotNull ArgumentConversionContext<Object> context,
@NonNull ClientRequestUriContext uriContext,
@NotNull Object value,
@NotNull MutableHttpRequest<?> request) {
if (value instanceof Map) {
for (Map.Entry<?, ?> entry: ((Map<?, ?>) value).entrySet()) {
String key = NameUtils.hyphenate(StringUtils.capitalize(entry.getKey().toString()), false);
request.header("X-Metadata-" + key, entry.getValue().toString());
}
}
}
}
Annotation Argument Binder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
import io.micronaut.http.client.bind.ClientRequestUriContext
import org.jetbrains.annotations.NotNull
import javax.inject.Singleton
@Singleton
class MetadataClientArgumentBinder implements AnnotatedClientArgumentRequestBinder<Metadata> {
Class<Metadata> annotationType = Metadata
@Override
void bind(@NotNull ArgumentConversionContext<Object> context,
@NonNull ClientRequestUriContext uriContext,
@NotNull Object value,
@NotNull MutableHttpRequest<?> request) {
if (value instanceof Map) {
for (def entry: ((Map) value).entrySet()) {
String key = NameUtils.hyphenate(StringUtils.capitalize(entry.key.toString()), false)
request.header("X-Metadata-" + key, entry.value.toString())
}
}
}
}
Annotation Argument Binder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.naming.NameUtils
import io.micronaut.core.util.StringUtils
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.AnnotatedClientArgumentRequestBinder
import io.micronaut.http.client.bind.ClientRequestUriContext
import javax.inject.Singleton
@Singleton
class MetadataClientArgumentBinder : AnnotatedClientArgumentRequestBinder<Metadata> {
override fun getAnnotationType(): Class<Metadata> {
return Metadata::class.java
}
override fun bind(context: ArgumentConversionContext<Any>,
uriContext: ClientRequestUriContext,
value: Any,
request: MutableHttpRequest<*>) {
if (value is Map<*, *>) {
for ((key1, value1) in value) {
val key = NameUtils.hyphenate(StringUtils.capitalize(key1.toString()), false)
request.header("X-Metadata-$key", value1.toString())
}
}
}
}
Binding By Type
To bind to the request based on the type of the argument, create a bean of type TypedClientArgumentRequestBinder.
In this example, see the following client:
Client With Metadata Argument
@Client("/")
public interface MetadataClient {
@Get("/client/bind")
String get(Metadata metadata);
}
Client With Metadata Argument
@Client("/")
interface MetadataClient {
@Get("/client/bind")
String get(Metadata metadata)
}
Client With Metadata Argument
@Client("/")
interface MetadataClient {
@Get("/client/bind")
operator fun get(metadata: Metadata?): String?
}
Without any additional code, the client will attempt to convert the metadata to a string and append it as a query parameter. In this case that isn’t the desired effect so a custom binder must be created.
The following binder will handle arguments passed to clients that are of the Metadata
type and then mutate the request to contain the desired headers.
Typed Argument Binder
import edu.umd.cs.findbugs.annotations.NonNull;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.client.bind.ClientRequestUriContext;
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder;
import javax.inject.Singleton;
@Singleton
public class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {
@Override
@NonNull
public Argument<Metadata> argumentType() {
return Argument.of(Metadata.class);
}
@Override
public void bind(@NonNull ArgumentConversionContext<Metadata> context,
@NonNull ClientRequestUriContext uriContext,
@NonNull Metadata value,
@NonNull MutableHttpRequest<?> request) {
request.header("X-Metadata-Version", value.getVersion().toString());
request.header("X-Metadata-Deployment-Id", value.getDeploymentId().toString());
}
}
Typed Argument Binder
import edu.umd.cs.findbugs.annotations.NonNull
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.ClientRequestUriContext
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder
import javax.inject.Singleton
@Singleton
class MetadataClientArgumentBinder implements TypedClientArgumentRequestBinder<Metadata> {
@Override
@NonNull
Argument<Metadata> argumentType() {
return Argument.of(Metadata)
}
@Override
void bind(@NonNull ArgumentConversionContext<Metadata> context,
@NonNull ClientRequestUriContext uriContext,
@NonNull Metadata value,
@NonNull MutableHttpRequest<?> request) {
request.header("X-Metadata-Version", value.version.toString())
request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
}
}
Typed Argument Binder
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.client.bind.ClientRequestUriContext
import io.micronaut.http.client.bind.TypedClientArgumentRequestBinder
import javax.inject.Singleton
@Singleton
class MetadataClientArgumentBinder : TypedClientArgumentRequestBinder<Metadata?> {
override fun argumentType(): Argument<Metadata?> {
return Argument.of(Metadata::class.java)
}
override fun bind(context: ArgumentConversionContext<Metadata?>,
uriContext: ClientRequestUriContext,
value: Metadata,
request: MutableHttpRequest<*>) {
request.header("X-Metadata-Version", value.version.toString())
request.header("X-Metadata-Deployment-Id", value.deploymentId.toString())
}
}