Spring HAL消费者返回空Resource问题及递归消费HAL端点需求
我正在尝试使用Spring HATEOAS的Jackson2HalModule结合RestTemplate来消费一个HAL格式的API端点,但返回的Resource始终是空的。以下是我的场景和代码:
示例HAL响应
{ "_embedded": { "machineStateClassAnswers": [ { "name": 1, "extendedTimeSegmentAnswer": { "duration": 24, "goodParts": 87, "rejectedParts": 16, "relativeDurationInPercentage": 17, "count": 7, "averageDuration": 3.5714 }, "_links": { "self": { "href": "http://localhost:1890/equipment/class_1?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" }, "class": { "href": "http://localhost:1890/equipment/class_1?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" } } }, { "name": 2, "extendedTimeSegmentAnswer": { "duration": 34, "goodParts": 54, "rejectedParts": 11, "relativeDurationInPercentage": 24, "count": 5, "averageDuration": 7.8 }, "_links": { "self": { "href": "http://localhost:1890/equipment/class_2?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" }, "class": { "href": "http://localhost:1890/equipment/class_2?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" } } }, { "name": 3, "extendedTimeSegmentAnswer": { "duration": 86, "goodParts": 76, "rejectedParts": 15, "relativeDurationInPercentage": 60, "count": 7, "averageDuration": 21.5714 }, "_links": { "self": { "href": "http://localhost:1890/equipment/class_3?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" }, "class": { "href": "http://localhost:1890/equipment/class_3?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" } } } ] }, "_links": { "self": { "href": "http://localhost:1890/equipment/classes?start=3&end=75&sap_keys=7000_3333&sap_keys=7000_4444" } } }
我的消费代码
Service类
@Service public class TableResponseService { @Value("${url}") String url; private final RestTemplate restTemplate; public TableResponseService(RestTemplateBuilder restTemplateBuilder) { ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.registerModule(new Jackson2HalModule()); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/hal+json")); converter.setObjectMapper(mapper); this.restTemplate = restTemplateBuilder.additionalMessageConverters(Arrays.asList(converter)).build(); } public Resources<List<MachineStateClassAnswer>> getMachineStates() { return restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Resources<List<MachineStateClassAnswer>>>() { }).getBody(); } }
数据类
public class MachineStateClassAnswer extends ResourceSupport { private Long name; public ExtendedTimeSegmentAnswer extendedTimeSegmentAnswer; public MachineStateClassAnswer(Long name, ExtendedTimeSegmentAnswer extendedTimeSegmentAnswer) { this.name = name; this.extendedTimeSegmentAnswer = extendedTimeSegmentAnswer; } // Getters and Setters public Long getName() { return name; } public ExtendedTimeSegmentAnswer getExtendedTimeSegmentAnswer() { return extendedTimeSegmentAnswer; } public void setName(Long name) { this.name = name; } public void setExtendedTimeSegmentAnswer(ExtendedTimeSegmentAnswer extendedTimeSegmentAnswer) { this.extendedTimeSegmentAnswer = extendedTimeSegmentAnswer; } }
public class ExtendedTimeSegmentAnswer extends BasicTimeSegmentAnswer { protected Long relativeDurationInPercentage; protected Long count; protected Double averageDuration; public ExtendedTimeSegmentAnswer() { } public ExtendedTimeSegmentAnswer(Long duration, Long goodParts, Long rejectedParts, Long count, Double averageDuration) { super(duration, goodParts, rejectedParts); this.count = count; this.averageDuration = averageDuration; } // Getters and Setters public Long getRelativeDurationInPercentage() { return relativeDurationInPercentage; } public Long getDuration() { return duration; } public Long getCount() { return count; } public void setCount(Long count) { this.count = count; } public Double getAverageDuration() { return averageDuration; } public void setAverageDuration(Double averageDuration) { this.averageDuration = averageDuration; } }
public class BasicTimeSegmentAnswer { protected Long duration; protected Long goodParts; protected Long rejectedParts; public BasicTimeSegmentAnswer() { } public BasicTimeSegmentAnswer(Long duration, Long goodParts, Long rejectedParts) { this.duration = duration; this.goodParts = goodParts; this.rejectedParts = rejectedParts; } // Getters and Setters public Long getDuration() { return duration; } public void setDuration(Long duration) { this.duration = duration; } public Long getGoodParts() { return goodParts; } public void setGoodParts(Long goodParts) { this.goodParts = goodParts; } public Long getRejectedParts() { return rejectedParts; } }
问题分析与修复方案
1. 错误的泛型类型定义
你当前使用的Resources<List<MachineStateClassAnswer>>是错误的,因为Resources本身就是Spring HATEOAS用来包裹集合资源的容器,它内部会自动处理_embedded中的数组。正确的泛型应该是Resources<MachineStateClassAnswer>,这样Jackson才能正确映射_embedded.machineStateClassAnswers到Resources的内容中。
2. 缺少HalHandlerInstantiator配置
仅仅注册Jackson2HalModule是不够的,还需要配置HalHandlerInstantiator来让Jackson正确实例化ResourceSupport的子类(比如你的MachineStateClassAnswer)。这个实例化器负责处理HAL链接的反序列化。
3. 数据类缺少无参构造函数
MachineStateClassAnswer只有带参数的构造函数,Jackson在反序列化时需要无参构造函数来创建对象实例。你需要添加无参构造函数,或者使用Lombok的@NoArgsConstructor注解。
4. 缺失字段的Setter方法
ExtendedTimeSegmentAnswer中的relativeDurationInPercentage字段只有Getter,没有Setter,这会导致该字段无法被反序列化。同时BasicTimeSegmentAnswer中的rejectedParts也缺少Setter方法。
修复后的代码
修复后的Service类
@Service public class TableResponseService { @Value("${url}") String url; private final RestTemplate restTemplate; public TableResponseService(RestTemplateBuilder restTemplateBuilder) { ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 注册HAL模块并配置实例化器 Jackson2HalModule halModule = new Jackson2HalModule(); HalHandlerInstantiator instantiator = new HalHandlerInstantiator( new RelProvider() { @Override public String getItemResourceRelFor(Class<?> type) { // 返回_embedded中的键名,这里是"machineStateClassAnswers" return "machineStateClassAnswers"; } @Override public String getCollectionResourceRelFor(Class<?> type) { return "machineStateClassAnswers"; } }, null, null); halModule.setHandlerInstantiator(instantiator); mapper.registerModule(halModule); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(mapper); // 设置支持的媒体类型,并确保优先级高于默认转换器 converter.setSupportedMediaTypes(Collections.singletonList(MediaType.parseMediaType("application/hal+json"))); // 将HAL转换器放在最前面 this.restTemplate = restTemplateBuilder .additionalMessageConverters(converter) .build(); } // 修改返回类型为Resources<MachineStateClassAnswer> public Resources<MachineStateClassAnswer> getMachineStates() { return restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<Resources<MachineStateClassAnswer>>() { }).getBody(); } }
修复后的MachineStateClassAnswer
public class MachineStateClassAnswer extends ResourceSupport { private Long name; public ExtendedTimeSegmentAnswer extendedTimeSegmentAnswer; // 添加无参构造函数 public MachineStateClassAnswer() { } public MachineStateClassAnswer(Long name, ExtendedTimeSegmentAnswer extendedTimeSegmentAnswer) { this.name = name; this.extendedTimeSegmentAnswer = extendedTimeSegmentAnswer; } // Getters and Setters public Long getName() { return name; } public ExtendedTimeSegmentAnswer getExtendedTimeSegmentAnswer() { return extendedTimeSegmentAnswer; } public void setName(Long name) { this.name = name; } public void setExtendedTimeSegmentAnswer(ExtendedTimeSegmentAnswer extendedTimeSegmentAnswer) { this.extendedTimeSegmentAnswer = extendedTimeSegmentAnswer; } }
修复后的ExtendedTimeSegmentAnswer
public class ExtendedTimeSegmentAnswer extends BasicTimeSegmentAnswer { protected Long relativeDurationInPercentage; protected Long count; protected Double averageDuration; public ExtendedTimeSegmentAnswer() { } public ExtendedTimeSegmentAnswer(Long duration, Long goodParts, Long rejectedParts, Long count, Double averageDuration) { super(duration, goodParts, rejectedParts); this.count = count; this.averageDuration = averageDuration; } // 添加relativeDurationInPercentage的Setter public Long getRelativeDurationInPercentage() { return relativeDurationInPercentage; } public void setRelativeDurationInPercentage(Long relativeDurationInPercentage) { this.relativeDurationInPercentage = relativeDurationInPercentage; } public Long getDuration() { return duration; } public Long getCount() { return count; } public void setCount(Long count) { this.count = count; } public Double getAverageDuration() { return averageDuration; } public void setAverageDuration(Double averageDuration) { this.averageDuration = averageDuration; } }
修复后的BasicTimeSegmentAnswer
public class BasicTimeSegmentAnswer { protected Long duration; protected Long goodParts; protected Long rejectedParts; public BasicTimeSegmentAnswer() { } public BasicTimeSegmentAnswer(Long duration, Long goodParts, Long rejectedParts) { this.duration = duration; this.goodParts = goodParts; this.rejectedParts = rejectedParts; } // 添加rejectedParts的Setter public Long getDuration() { return duration; } public void setDuration(Long duration) { this.duration = duration; } public Long getGoodParts() { return goodParts; } public void setGoodParts(Long goodParts) { this.goodParts = goodParts; } public Long getRejectedParts() { return rejectedParts; } public void setRejectedParts(Long rejectedParts) { this.rejectedParts = rejectedParts; } }
额外提示
- 如果你的Spring Boot版本是2.x及以上,可以考虑使用
RestTemplateCustomizer来统一配置HAL转换器,避免在每个Service中重复配置。 - 可以通过
resources.getContent()来获取MachineStateClassAnswer的集合列表,遍历处理每个资源。
内容的提问来源于stack exchange,提问作者PeMa

