You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Micronaut GET方法复杂POJO参数引发400错误及自定义Binder未生效问题

问题:Micronaut中GET请求复杂POJO查询参数绑定失败,自定义Binder未执行

问题场景

在Micronaut框架中,将FilterSortRequest这类包含集合类型的复杂POJO作为GET请求的查询参数时,接口返回400 Bad Request错误,且自定义的FilterSortRequestBinder的bind方法从未被执行。


实体类定义

@Serdeable
public record FilterSortRequest(@JsonProperty("filters")List<FilterOption> filters, @JsonProperty("sorts")Map<String, SortOperators> sorts) {
}

控制器代码

@Controller("/tag")
@Version("1")
@Tag(name = "Tag")
public class TagController implements IHttpAction<TagRequest, TagResponse> {
   
    public TagController(IServiceAction<FilterSortRequest, TagRequest, TagResponse> iServiceAction) {
        this.iServiceAction = iServiceAction;
    }

    @Override
    public HttpResponse<?> find(@QueryValue FilterSortRequest searchCriteria) {
        var result = this.iServiceAction.find(searchCriteria);
        return Result.match(()-> HttpResponse.ok(result.value), ()->HttpResponse.notFound(result.exception.getMessage()));
    }
}

HTTP客户端接口

@Validated
@ExecuteOn(TaskExecutors.BLOCKING)
public interface IHttpAction<C, T, R> {

    @Get(value = "/{?searchCriteria*}")
    HttpResponse<?> find(@QueryValue FilterSortRequest searchCriteria);


    @Get(uri = "/{id}")
    HttpResponse<?> get(@PathVariable(name = "id") @Nonnull UUID id);


    @Post
    HttpResponse<?> post(@Body T request);


    @Put(uri = "/{id}")
    HttpResponse<?> put(@PathVariable(name = "id") @NotNull UUID id, @Body T request);

    @Delete(uri = "/{id}")
    HttpResponse<?> delete(@PathVariable(name = "id") @NotNull UUID id);
}

自定义Binder实现

@Singleton
public class FilterSortRequestBinder implements TypedRequestArgumentBinder<FilterSortRequest> {

    private final ObjectMapper objectMapper;

    public FilterSortRequestBinder(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Argument<FilterSortRequest> argumentType() {
        return Argument.of(FilterSortRequest.class);
    }

    @Override
    public BindingResult<FilterSortRequest> bind(ArgumentConversionContext<FilterSortRequest> context, HttpRequest<?> source) {
        try {
            // Extract the 'filters' parameter
            String filtersParam = source.getParameters().get("filters");
            // Extract the 'sorts' parameter
            String sortsParam = source.getParameters().get("sorts");

            // Deserialize filters and sorts
            List<FilterOption> filters = filtersParam != null
                    ? objectMapper.readValue(filtersParam, Argument.listOf(FilterOption.class))
                    : Collections.emptyList();

            Map<String, SortOperators> sorts = sortsParam != null
                    ? objectMapper.readValue(sortsParam, Argument.mapOf(String.class, SortOperators.class))
                    : Collections.emptyMap();

            // Create the FilterSortRequest object
            FilterSortRequest filterSortRequest = new FilterSortRequest(filters, sorts);
            return () -> Optional.of(filterSortRequest);
        } catch (Exception e) {
            e.printStackTrace();
            return BindingResult.EMPTY;
        }
    }
}

请求示例

http://localhost:8080/tag?filters=[{"field":"string","operator":"EQUAL","values":{"additionalProp1":"string","additionalProp2":"string","additionalProp3":"string"}}]&sorts={"additionalProp1":"ASC","additionalProp2":"ASC","additionalProp3":"ASC"}

错误响应

{
  "_links": {
    "self": [
      {
        "href": "/tag?filters=%5B%7B%22field%22%3A%22string%22%2C%22operator%22%3A%22EQUAL%22%2C%22values%22%3A%7B%22additionalProp1%22%3A%22string%22%2C%22additionalProp2%22%3A%22string%22%2C%22additionalProp3%22%3A%22string%22%7D%7D%5D&sorts=%7B%22additionalProp1%22%3A%22ASC%22%2C%22additionalProp2%22%3A%22ASC%22%2C%22additionalProp3%22%3A%22ASC%22%7D",
        "templated": false
      }
    ]
  },
  "_embedded": {
    "errors": [
      {
        "message": "Failed to convert argument [searchCriteria] for value [null] due to: Unexpected token VALUE_STRING, expected START_OBJECT \n at [Source: UNKNOWN; byte offset: #UNKNOWN]",
        "path": "/searchCriteria"
      }
    ]
  },
  "message": "Bad Request"
}

问题原因及修复方案

核心原因

  1. Micronaut默认优先使用内置序列化器(如Jackson)绑定复杂对象,而请求中是将FilterSortRequest拆分为filters和sorts两个独立查询参数,并非完整的JSON对象,内置转换器尝试转换时直接失败,返回400,未触发自定义Binder。
  2. 控制器参数的@QueryValue未设置required = false,内置转换失败时直接抛出错误,不会降级到自定义Binder逻辑。

修复步骤

  1. 指定控制器参数使用自定义Binder
    修改控制器find方法的参数注解,明确绑定自定义Binder,并设置required = false避免提前报错:

    @Override
    public HttpResponse<?> find(@QueryValue(required = false) @Bindable(binder = FilterSortRequestBinder.class) FilterSortRequest searchCriteria) {
        var result = this.iServiceAction.find(searchCriteria);
        return Result.match(()-> HttpResponse.ok(result.value), ()->HttpResponse.notFound(result.exception.getMessage()));
    }
    
  2. 提升自定义Binder优先级
    给自定义Binder添加@Primary注解,确保Micronaut优先使用该Binder而非内置转换器:

    @Singleton
    @Primary
    public class FilterSortRequestBinder implements TypedRequestArgumentBinder<FilterSortRequest> {
        // 原有逻辑保持不变
    }
    
  3. 调试与优化Binder逻辑
    将bind方法中的e.printStackTrace()替换为日志框架记录,便于排查绑定失败原因:

    catch (Exception e) {
        LoggerFactory.getLogger(FilterSortRequestBinder.class).error("绑定FilterSortRequest失败", e);
        return BindingResult.EMPTY;
    }
    
  4. 移除冲突注解
    移除FilterSortRequest上的@JsonProperty注解,避免Micronaut Serde与Jackson注解冲突:

    @Serdeable
    public record FilterSortRequest(List<FilterOption> filters, Map<String, SortOperators> sorts) {
    }
    

内容的提问来源于stack exchange,提问作者San Jaisy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 11:13:11