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接口的类型安全优势;转换过程会消耗额外性能。
更推荐的替代方案
除上述思路外,还有两种更高效的解决方式:
- 自定义Jackson序列化器:为
ImpactOverview接口编写自定义序列化器,先执行所有字段加载逻辑,确认无异常后再写入类型前缀和完整JSON,避免先写类型字段再抛异常的问题; - 在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

