6.5 Custom Argument Binding
Micronaut uses an ArgumentBinderRegistry to look up ArgumentBinders beans capable of binding to the arguments in the controller methods. The default implementation will look for an annotation on the argument that is, itself, annotated with @Bindable. If one exists the argument binder registry will search for an argument binder that supports that annotation.
If no fitting annotation is found Micronaut will try to find an argument binder that supports the argument’s type.
An argument binder returns a BindingResult. The binding result gives Micronaut more information than just the value. Binding results are either satisfied or unsatisfied, and either empty or not empty. If an argument binder returns an unsatisfied result, the binder may be called again at different times in the request processing. Argument binders are initially called before the body is read and before any filters are executed. If a binder relies on any of that data and the data is not present, then an UNSATISFIED result should be returned. Returning an EMPTY or satisfied result will be the final result and the binder will not be called again for that request.
At the end of processing if the result is still UNSATISFIED, it is considered EMPTY. |
Key interfaces are:
AnnotatedRequestArgumentBinder
Argument binders that bind based on the presence of an annotation must implement AnnotatedRequestArgumentBinder, and can be used by creating an annotation that is annotated with @Bindable. For example:
An example of a binding annotation
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.bind.annotation.Bindable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RUNTIME)
@Bindable (1)
public @interface ShoppingCart {
@AliasFor(annotation = Bindable.class, member = "value")
String value() default "";
}
An example of a binding annotation
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.AliasFor
import io.micronaut.core.bind.annotation.Bindable
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.Target
import static java.lang.annotation.RetentionPolicy.RUNTIME
@CompileStatic
@Target([ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE])
@Retention(RUNTIME)
@Bindable (1)
@interface ShoppingCart {
@AliasFor(annotation = Bindable.class, member = "value")
String value() default ""
}
An example of a binding annotation
import io.micronaut.core.bind.annotation.Bindable
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.ANNOTATION_CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Bindable (1)
annotation class ShoppingCart(val value: String = "")
1 | The binding annotation must itself be annotated as @Bindable |
Example of annotated data binding
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.jackson.serialize.JacksonObjectSerializer;
import javax.inject.Singleton;
import java.util.Map;
import java.util.Optional;
@Singleton
public class ShoppingCartRequestArgumentBinder implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { (1)
private final ConversionService<?> conversionService;
private final JacksonObjectSerializer objectSerializer;
public ShoppingCartRequestArgumentBinder(ConversionService<?> conversionService, JacksonObjectSerializer objectSerializer) {
this.conversionService = conversionService;
this.objectSerializer = objectSerializer;
}
@Override
public Class<ShoppingCart> getAnnotationType() {
return ShoppingCart.class;
}
@Override
public BindingResult<Object> bind(
ArgumentConversionContext<Object> context,
HttpRequest<?> source) { (2)
String parameterName = context.getAnnotationMetadata()
.stringValue(ShoppingCart.class)
.orElse(context.getArgument().getName());
Cookie cookie = source.getCookies().get("shoppingCart");
if (cookie != null) {
Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
cookie.getValue().getBytes(),
Argument.mapOf(String.class, Object.class));
return () -> cookieValue.flatMap(map -> {
Object obj = map.get(parameterName);
return conversionService.convert(obj, context);
});
}
return BindingResult.EMPTY;
}
}
Example of annotated data binding
import groovy.transform.CompileStatic
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
import io.micronaut.http.cookie.Cookie
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import javax.inject.Singleton
@CompileStatic
@Singleton
class ShoppingCartRequestArgumentBinder implements AnnotatedRequestArgumentBinder<ShoppingCart, Object> { (1)
private final ConversionService<?> conversionService
private final JacksonObjectSerializer objectSerializer
ShoppingCartRequestArgumentBinder(
ConversionService<?> conversionService,
JacksonObjectSerializer objectSerializer) {
this.conversionService = conversionService
this.objectSerializer = objectSerializer
}
@Override
Class<ShoppingCart> getAnnotationType() {
return ShoppingCart.class
}
@Override
BindingResult<Object> bind(
ArgumentConversionContext<Object> context,
HttpRequest<?> source) { (2)
String parameterName = context.annotationMetadata
.stringValue(ShoppingCart)
.orElse(context.argument.name)
Cookie cookie = source.cookies.get("shoppingCart")
if (cookie != null) {
Optional<Map<String, Object>> cookieValue = objectSerializer.deserialize(
cookie.value.bytes,
Argument.mapOf(String, Object))
return (BindingResult) { ->
cookieValue.flatMap({value ->
conversionService.convert(value.get(parameterName), context)
})
}
}
return BindingResult.EMPTY
}
}
Example of annotated data binding
import io.micronaut.core.bind.ArgumentBinder.BindingResult
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.convert.ConversionService
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.AnnotatedRequestArgumentBinder
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import java.util.*
import javax.inject.Singleton
@Singleton
class ShoppingCartRequestArgumentBinder(
private val conversionService: ConversionService<*>,
private val objectSerializer: JacksonObjectSerializer
) : AnnotatedRequestArgumentBinder<ShoppingCart, Any> { (1)
override fun getAnnotationType(): Class<ShoppingCart> {
return ShoppingCart::class.java
}
override fun bind(context: ArgumentConversionContext<Any>, source: HttpRequest<*>): BindingResult<Any> { (2)
val parameterName = context.annotationMetadata
.stringValue(ShoppingCart::class.java)
.orElse(context.argument.name)
val cookie = source.cookies.get("shoppingCart")
if (cookie != null) {
val cookieValue: Optional<Map<String, Any>> = objectSerializer.deserialize(
cookie.value.toByteArray(),
Argument.mapOf(String::class.java, Any::class.java))
return BindingResult {
cookieValue.flatMap { map: Map<String, Any> ->
conversionService.convert(map[parameterName], context)
}
}
}
return BindingResult.EMPTY
}
}
1 | The custom argument binder must implement AnnotatedRequestArgumentBinder, including both the annotation type to trigger the binder (in this case, MyBindingAnnotation) and the type of the argument expected (in this case, Object) |
2 | Override the bind method with the custom argument binding logic - in this case, we are resolving the name of the annotated argument, extracting a value from a cookie with that same name, and converting that value into the argument type |
It is common to use ConversionService to convert the data to the type of the argument. |
Once the binder is created, we can annotate an argument in our controller method, which will be bound using the custom binding logic we’ve specified.
A controller operation with this annotated binding
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
return HttpResponse.ok("Session:" + sessionId);
}
// end::method
}
A controller operation with this annotated binding
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { (1)
HttpResponse.ok("Session:${sessionId}".toString())
}
// end::method
}
A controller operation with this annotated binding
@Get("/annotated")
fun checkSession(@ShoppingCart sessionId: Long): HttpResponse<String> { (1)
return HttpResponse.ok("Session:$sessionId")
}
1 | The parameter will be bound with the binder associated with MyBindingAnnotation. This will take precedence over a type-based binder, if applicable. |
TypedRequestArgumentBinder
Argument binders that bind based on the type of the argument must implement TypedRequestArgumentBinder. For example, given this class:
Example of POJO
import io.micronaut.core.annotation.Introspected;
@Introspected
public class ShoppingCart {
private String sessionId;
private Integer total;
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}
}
Example of POJO
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
String sessionId
Integer total
}
Example of POJO
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
var sessionId: String? = null
var total: Int? = null
}
We can define a TypedRequestArgumentBinder
for this class, as seen below:
Example of typed data binding
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.type.Argument;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder;
import io.micronaut.http.cookie.Cookie;
import io.micronaut.jackson.serialize.JacksonObjectSerializer;
import javax.inject.Singleton;
import java.util.Optional;
@Singleton
public class ShoppingCartRequestArgumentBinder implements TypedRequestArgumentBinder<ShoppingCart> {
private final JacksonObjectSerializer objectSerializer;
public ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
this.objectSerializer = objectSerializer;
}
@Override
public BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context, HttpRequest<?> source) { (1)
Cookie cookie = source.getCookies().get("shoppingCart");
if (cookie != null) {
return () -> objectSerializer.deserialize( (2)
cookie.getValue().getBytes(),
ShoppingCart.class);
}
return Optional::empty;
}
@Override
public Argument<ShoppingCart> argumentType() {
return Argument.of(ShoppingCart.class); (3)
}
}
Example of typed data binding
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.http.cookie.Cookie
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import javax.inject.Singleton
@Singleton
class ShoppingCartRequestArgumentBinder implements TypedRequestArgumentBinder<ShoppingCart> {
private final JacksonObjectSerializer objectSerializer
ShoppingCartRequestArgumentBinder(JacksonObjectSerializer objectSerializer) {
this.objectSerializer = objectSerializer
}
@Override
BindingResult<ShoppingCart> bind(ArgumentConversionContext<ShoppingCart> context, HttpRequest<?> source) { (1)
Cookie cookie = source.getCookies().get("shoppingCart")
if (cookie != null) {
return () -> objectSerializer.deserialize( (2)
cookie.getValue().getBytes(),
ShoppingCart)
}
return BindingResult.EMPTY
}
@Override
Argument<ShoppingCart> argumentType() {
Argument.of(ShoppingCart) (3)
}
}
Example of typed data binding
import io.micronaut.core.bind.ArgumentBinder
import io.micronaut.core.bind.ArgumentBinder.BindingResult
import io.micronaut.core.convert.ArgumentConversionContext
import io.micronaut.core.type.Argument
import io.micronaut.http.HttpRequest
import io.micronaut.http.bind.binders.TypedRequestArgumentBinder
import io.micronaut.jackson.serialize.JacksonObjectSerializer
import javax.inject.Singleton
import java.util.Optional
@Singleton
class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) :
TypedRequestArgumentBinder<ShoppingCart> {
override fun bind(
context: ArgumentConversionContext<ShoppingCart>,
source: HttpRequest<*>
): ArgumentBinder.BindingResult<ShoppingCart> { (1)
val cookie = source.cookies["shoppingCart"]
return if (cookie != null) {
BindingResult {
objectSerializer.deserialize( (2)
cookie.value.toByteArray(),
ShoppingCart::class.java
)
}
} else BindingResult {
Optional.empty()
}
}
override fun argumentType(): Argument<ShoppingCart> {
return Argument.of(ShoppingCart::class.java) (3)
}
}
1 | Override the bind method with the data type to bind, in this example the ShoppingCart type |
2 | After retrieving the data (in this case, by deserializing JSON text from a cookie), return as a BindingResult |
3 | Also override the argumentType method, which is used by the ArgumentBinderRegistry. |
Once the binder is created, it will be used for any controller argument of the associated type:
A controller operation with this typed binding
@Get("/typed")
public HttpResponse<?> loadCart(ShoppingCart shoppingCart) { (1)
Map<String, Object> responseMap = new HashMap<>();
responseMap.put("sessionId", shoppingCart.getSessionId());
responseMap.put("total", shoppingCart.getTotal());
return HttpResponse.ok(responseMap);
}
A controller operation with this typed binding
@Get("/typed")
HttpResponse<Map<String, Object>> loadCart(ShoppingCart shoppingCart) { (1)
Map<String, Object> responseMap = [:]
responseMap.sessionId = shoppingCart.sessionId
responseMap.total = shoppingCart.total
return HttpResponse.ok(responseMap)
}
A controller operation with this typed binding
@Get("/typed")
fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { (1)
val responseMap: MutableMap<String, Any?> = HashMap()
responseMap["sessionId"] = shoppingCart.sessionId
responseMap["total"] = shoppingCart.total
return HttpResponse.ok<Map<String, Any?>>(responseMap)
}
1 | The parameter will be bound using the custom logic defined for this type in our TypedRequestArgumentBinder |