Spring Data JPA投影接口在WebTestClient中序列化失败问题求助
问题描述
定义了Spring Data JPA投影接口:
public interface BloodUnits { BloodType getBloodType(); Integer getTotalUnits(); }
仓库中实现查询方法:
@Repository public interface BloodDonationsRepository extends JpaRepository<BloodDonations, Long> { @Query(value = "SELECT u.bloodType AS bloodType, SUM(bd.units) AS totalUnits " + "FROM blood_donations bd, users u WHERE bd.donor.id = u.id GROUP BY u.bloodType") List<BloodUnits> countAvailableUnitsByBloodType(); }
Postman调用对应接口端点正常,但用WebTestClient测试时:
@SpringBootTest(webEnvironment = RANDOM_PORT) public class BloodDonationsControllerTest { @Autowired WebTestClient webTestClient; // ...省略其他代码 List<BloodUnits> availableBloodUnits = webTestClient.get() .uri(bloodDonationURI + "/admin/available_blood_units") .accept(MediaType.APPLICATION_JSON) .headers(h -> h.setBearerAuth(jwt)) .exchange() .expectStatus() .isOk() .expectBodyList(new ParameterizedTypeReference<BloodUnits>(){}) .returnResult() .getResponseBody(); // ...省略其他代码 }
抛出错误:
org.springframework.core.codec.CodecException: Type definition error: [simple type, class com.bds.dto.BloodUnits] ... Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.bds.dto.BloodUnits` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information...
尝试将投影接口移至实体目录未解决问题,疑惑为何Postman调用正常但WebTestClient测试无法创建实例。
问题原因
- Postman调用时,后端返回的是JSON字符串,Postman仅负责展示内容,不需要把JSON反序列化成
BloodUnits接口实例——服务端的Spring Data JPA确实会为投影接口生成代理实例,但这个实例序列化后就变成了普通JSON,Postman无需处理接口实例的构造逻辑。 - WebTestClient测试时,是在客户端侧把响应的JSON反序列化成
BloodUnits接口类型。Jackson无法直接构造抽象类型(接口)的实例,因为接口没有构造方法,而Spring Data生成投影代理的逻辑仅在服务端生效,客户端侧没有这个代理生成机制,因此报错。
解决方法
方法1:改用具体DTO类替代投影接口
创建普通POJO类替代投影接口,Spring Data JPA同样支持将查询结果映射到这类类上:
public class BloodUnitsDTO { private BloodType bloodType; private Integer totalUnits; // 必须提供与查询结果字段匹配的构造方法 public BloodUnitsDTO(BloodType bloodType, Integer totalUnits) { this.bloodType = bloodType; this.totalUnits = totalUnits; } // getter方法 public BloodType getBloodType() { return bloodType; } public Integer getTotalUnits() { return totalUnits; } }
修改仓库方法的返回类型:
List<BloodUnitsDTO> countAvailableUnitsByBloodType();
测试代码中也改用BloodUnitsDTO:
List<BloodUnitsDTO> availableBloodUnits = webTestClient.get() .uri(bloodDonationURI + "/admin/available_blood_units") .accept(MediaType.APPLICATION_JSON) .headers(h -> h.setBearerAuth(jwt)) .exchange() .expectStatus() .isOk() .expectBodyList(BloodUnitsDTO.class) .returnResult() .getResponseBody();
方法2:测试时用Map接收响应
如果不想修改投影接口,可以在测试中将响应反序列化成Map,再手动处理数据:
List<Map<String, Object>> responseMaps = webTestClient.get() .uri(bloodDonationURI + "/admin/available_blood_units") .accept(MediaType.APPLICATION_JSON) .headers(h -> h.setBearerAuth(jwt)) .exchange() .expectStatus() .isOk() .expectBodyList(Map.class) .returnResult() .getResponseBody(); // 手动将Map转成需要的结构 List<BloodUnits> availableBloodUnits = responseMaps.stream() .map(map -> new BloodUnits() { @Override public BloodType getBloodType() { return BloodType.valueOf((String) map.get("bloodType")); } @Override public Integer getTotalUnits() { return ((Number) map.get("totalUnits")).intValue(); } }) .collect(Collectors.toList());
方法3:配置Jackson支持接口反序列化(不推荐)
可以给BloodUnits接口添加@JsonDeserialize注解指定自定义反序列化类,或者配置Spring消息转换器,但这种方式配置复杂,不如直接用DTO类简洁。
内容的提问来源于stack exchange,提问作者mls
相关产品推荐
相关产品推荐

