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

Spring Open Projections+Jackson:@Value抛异常时仍输出JsonType注解的问题

项目背景

我正在使用带类型名称注解的Open Projections,Impacts是一个递归接口,为所有存在影响的对象添加impacts属性。相关精简代码如下:

@GetMapping("/{id}/impacts")
public ImpactOverview getImpacts(@PathVariable("id") String id) {
    return repository.findImpactByLabel(id, Impacts.class);
}
public interface Impacts extends Field, ImpactOverview {
    // repo 是简单的Spring Data Repository,包含id和投影类参数
    @JsonIgnore
    @Value("#{@repo.findTable(target.label, T(com.company.view.projections.field.impacts.Table))}")
    Table getTable();

    default Collection<? extends ImpactOverview> impacts() {
        // combine方法用于将集合字段和单个字段合并为一个大集合
        return combine(getTable(), ...);
    }
}
public interface Table extends com.company.view.projections.table.Table, ImpactOverview {}
@JsonTypeName(AssetType.FIELD_TYPENAME)
public interface Field {}
@JsonTypeName(AssetType.TABLE_TYPENAME)
public interface Table {}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface ImpactOverview {
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
    default Collection<? extends ImpactOverview> impacts() {
        return null;
    }
}

正常情况下,该实现会生成嵌套的ImpactOverview JSON结构,清晰展示字段变更带来的递归影响关系。同时我已编写继承ResponseEntityExceptionHandler的ControllerAdvice统一处理异常:

@ControllerAdvice
@Slf4j
@RequiredArgsConstructor
public class ControllerExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(final Exception ex,
                                                             final Object body,
                                                             final HttpHeaders headers,
                                                             final HttpStatusCode statusCode,
                                                             final WebRequest request) {
        return toCompanyException(ex);
    }

    @ResponseBody
    ResponseEntity<Object> toCompanyException(@NonNull Exception e) {
        return ResponseEntity.status(Status.of(e)).body(Body.of(e));
    }
}

问题现象

当repo.findTable调用失败时,AbstractJackson2HttpMessageConverter会抛出HttpMessageNotWritable异常,此时返回的JSON结构同时包含类型字段与异常对象:

{
    "type": "FIELD"
}{
    "exception": "http-message-not-writable",
    "message": "exception"
}

原因在于BeanSerializerBase::serializeWithType方法会先执行writeTypePrefix写入"type":"FIELD",之后调用serializeFields时才抛出异常,导致已写入的类型字段无法回滚。

需求与疑问

我希望仅返回异常对象,针对以下问题寻求可行解决方案:

1. 提前调用所有getImpacts方法会导致查询执行两次吗?

会。Spring Data Open Projections的代理实现是懒加载机制:只有调用接口方法时,才会执行对应的SpEL表达式或数据查询。如果在Controller里提前调用impacts()触发数据加载,后续Jackson序列化时还会再调用一次,等于查询执行两次,会额外增加数据库压力,不推荐这种做法。

2. 接口代理是否存在缓存机制?

默认不存在。Spring Data生成的投影代理不会缓存方法调用结果,每次调用接口方法(比如getTable()或impacts()),都会重新执行对应的逻辑(包括SpEL表达式里的repo.findTable查询)。如果需要缓存,可以在Repository的查询方法上添加@Cacheable注解,或者在SpEL表达式中手动实现缓存逻辑,避免重复查询。

3. 是否应该将数据存入Map<String, Object>后再由Jackson解析?

这是一个可行方案,但需要权衡成本:

  • 优点:提前将投影对象转换为Map(可通过Jackson的ObjectMapper.convertValue()实现),转换过程会触发所有懒加载逻辑,若出现异常可直接在Controller层捕获返回异常响应,不会出现序列化时写入部分JSON的问题;
  • 缺点:需要手动处理递归结构转换,增加代码复杂度;失去Projection接口的类型安全优势;转换过程会消耗额外性能。

更推荐的替代方案

除上述思路外,还有两种更高效的解决方式:

  1. 自定义Jackson序列化器:为ImpactOverview接口编写自定义序列化器,先执行所有字段加载逻辑,确认无异常后再写入类型前缀和完整JSON,避免先写类型字段再抛异常的问题;
  2. 在Projection层面捕获异常:修改Impacts接口的impacts()默认方法,调用getTable()时捕获异常,返回空集合或自定义错误标记,避免异常传递到Jackson序列化阶段,示例代码:
default Collection<? extends ImpactOverview> impacts() {
    try {
        return combine(getTable(), ...);
    } catch (Exception e) {
        log.error("Failed to load impacts", e);
        return Collections.emptyList();
    }
}

内容的提问来源于stack exchange,提问作者Jente Sondervorst

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 17:32:45