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

Spring HAL消费者返回空Resource问题及递归消费HAL端点需求

解决Spring HATEOAS消费HAL端点返回空Resource的问题

我正在尝试使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 10:18:58