Micronaut 自定义参数绑定
Micronaut 使用 ArgumentBinderRegistry 来查找能够绑定到控制器方法中的参数的 ArgumentBinder beans。默认实现在使用@Bindable 进行元注释的参数上查找注释。如果存在,则参数绑定器注册表会搜索支持该注释的参数绑定器。
如果未找到合适的注解,Micronaut 会尝试寻找支持参数类型的参数绑定器。
参数绑定器返回 ArgumentBinder.BindingResult。绑定结果为 Micronaut 提供了比值更多的信息。绑定结果要么满足要么不满足,要么为空要么不为空。如果参数绑定器返回不满意的结果,则可能会在请求处理的不同时间再次调用绑定器。参数绑定器最初在读取主体之前和执行任何过滤器之前调用。如果活页夹依赖于任何该数据但不存在,则返回 ArgumentBinder.BindingResult#UNSATISFIED 结果。返回 ArgumentBinder.BindingResult#EMPTY 或满意的结果将是最终结果,并且不会为该请求再次调用活页夹。
处理结束时,如果结果仍然是ArgumentBinder.BindingResult#UNSATISFIED,则认为是ArgumentBinder.BindingResult#EMPTY。
关键接口有:
AnnotatedRequestArgumentBinder
基于注解的存在进行绑定的参数绑定器必须实现 AnnotatedRequestArgumentBinder,并且可以通过创建使用 Bindable 进行注解的注解来使用。例如:
绑定注释的示例
Java |
Groovy |
Kotlin |
import io.micronaut.context.annotation.AliasFor;
import io.micronaut.core.bind.annotation.Bindable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target({FIELD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Bindable //(1)
public @interface ShoppingCart {
@AliasFor(annotation = Bindable.class, member = "value")
String value() default "";
}
|
import groovy.transform.CompileStatic
import io.micronaut.context.annotation.AliasFor
import io.micronaut.core.bind.annotation.Bindable
import java.lang.annotation.Retention
import java.lang.annotation.Target
import static java.lang.annotation.ElementType.ANNOTATION_TYPE
import static java.lang.annotation.ElementType.FIELD
import static java.lang.annotation.ElementType.PARAMETER
import static java.lang.annotation.RetentionPolicy.RUNTIME
@CompileStatic
@Target([FIELD, PARAMETER, ANNOTATION_TYPE])
@Retention(RUNTIME)
@Bindable //(1)
@interface ShoppingCart {
@AliasFor(annotation = Bindable, member = "value")
String value() default ""
}
|
import io.micronaut.core.bind.annotation.Bindable
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER
@Target(FIELD, VALUE_PARAMETER, ANNOTATION_CLASS)
@Retention(RUNTIME)
@Bindable //(1)
annotation class ShoppingCart(val value: String = "")
|
绑定注解本身必须被注解为 Bindable
带注释的数据绑定示例
Java |
Groovy |
Kotlin |
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 jakarta.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) {
return BindingResult.EMPTY;
}
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);
});
}
}
|
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 jakarta.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() {
ShoppingCart
}
@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) {
return BindingResult.EMPTY
}
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)
})
}
}
}
|
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.Optional
import jakarta.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") ?: return BindingResult.EMPTY
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)
}
}
}
}
|
自定义参数绑定器必须实现 AnnotatedRequestArgumentBinder,包括触发绑定器的注释类型(在本例中为 MyBindingAnnotation)和预期的参数类型(在本例中为 Object)
使用自定义参数绑定逻辑覆盖 bind 方法 - 在这种情况下,我们解析带注释的参数的名称,从具有相同名称的 cookie 中提取一个值,并将该值转换为参数类型
通常使用 ConversionService 将数据转换为参数的类型。
创建活页夹后,我们可以在我们的控制器方法中注释一个参数,该方法将使用我们指定的自定义逻辑进行绑定。
具有此注释绑定的控制器操作
Java |
Groovy |
Kotlin |
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { //(1)
return HttpResponse.ok("Session:" + sessionId);
}
// end::method
}
|
@Get("/annotated")
HttpResponse<String> checkSession(@ShoppingCart Long sessionId) { //(1)
HttpResponse.ok("Session:" + sessionId)
}
// end::method
}
|
@Get("/annotated")
fun checkSession(@ShoppingCart sessionId: Long): HttpResponse<String> { //(1)
return HttpResponse.ok("Session:$sessionId")
}
|
该参数与与 MyBindingAnnotation 关联的活页夹绑定。如果适用,这优先于基于类型的活页夹。
TypedRequestArgumentBinder
基于参数类型绑定的参数绑定器必须实现 TypedRequestArgumentBinder。例如,给定这个类:
POJO 的例子
Java |
Groovy |
Kotlin |
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;
}
}
|
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
String sessionId
Integer total
}
|
import io.micronaut.core.annotation.Introspected
@Introspected
class ShoppingCart {
var sessionId: String? = null
var total: Int? = null
}
|
我们可以为这个类定义一个 TypedRequestArgumentBinder,如下所示:
类型化数据绑定示例
Java |
Groovy |
Kotlin |
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 jakarta.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 Optional::empty;
}
return () -> objectSerializer.deserialize( //(2)
cookie.getValue().getBytes(),
ShoppingCart.class);
}
@Override
public Argument<ShoppingCart> argumentType() {
return Argument.of(ShoppingCart.class); //(3)
}
}
|
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 jakarta.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.cookies.get("shoppingCart")
if (!cookie) {
return BindingResult.EMPTY
}
return () -> objectSerializer.deserialize( //(2)
cookie.value.bytes,
ShoppingCart)
}
@Override
Argument<ShoppingCart> argumentType() {
Argument.of(ShoppingCart) //(3)
}
}
|
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 java.util.Optional
import jakarta.inject.Singleton
@Singleton
class ShoppingCartRequestArgumentBinder(private val objectSerializer: JacksonObjectSerializer) :
TypedRequestArgumentBinder<ShoppingCart> {
override fun bind(
context: ArgumentConversionContext<ShoppingCart>,
source: HttpRequest<*>
): BindingResult<ShoppingCart> { //(1)
val cookie = source.cookies["shoppingCart"]
return if (cookie == null)
BindingResult {
Optional.empty()
}
else {
BindingResult {
objectSerializer.deserialize( // (2)
cookie.value.toByteArray(),
ShoppingCart::class.java
)
}
}
}
override fun argumentType(): Argument<ShoppingCart> {
return Argument.of(ShoppingCart::class.java) //(3)
}
}
|
使用要绑定的数据类型覆盖绑定方法,在本例中为 ShoppingCart 类型
检索数据后(在本例中,通过反序列化 cookie 中的 JSON 文本),作为 ArgumentBinder.BindingResult 返回
还要覆盖 ArgumentBinderRegistry 使用的 argumentType 方法。
创建活页夹后,它将用于关联类型的任何控制器参数:
具有此类型绑定的控制器操作
Java |
Groovy |
Kotlin |
@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);
}
|
@Get("/typed")
HttpResponse<Map<String, Object>> loadCart(ShoppingCart shoppingCart) { //(1)
HttpResponse.ok(
sessionId: shoppingCart.sessionId,
total: shoppingCart.total)
}
|
@Get("/typed")
fun loadCart(shoppingCart: ShoppingCart): HttpResponse<*> { //(1)
return HttpResponse.ok(mapOf(
"sessionId" to shoppingCart.sessionId,
"total" to shoppingCart.total))
}
|
该参数使用在我们的 TypedRequestArgumentBinder 中为此类型定义的自定义逻辑进行绑定