Spring 3.0.0中RestTemplate无法反序列化JSON数组问题排查
问题描述
在Spring 3.0.0环境下,使用RestTemplate调用批量查询接口http://localhost:8080/data-api/ingredients时抛出反序列化错误,但通过ID调用单个对象接口http://localhost:8080/data-api/ingredients/{id}时功能正常。
错误栈
Caused by: org.springframework.web.client.RestClientException: 提取类型为[class [Lru.sandwichcloud.Ingredient;]、内容类型为[application/json]的响应时出错
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:118) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1132) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1115) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:865) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:764) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:405) ~[spring-web-6.0.2.jar!/:6.0.2]
at ru.sandwichcloud.SandwichCloudClient.getAllIngredients(SandwichCloudClient.java:42) ~[classes!/:0.0.7-SNAPSHOT]
at ru.sandwichcloud.RestExamples.lambda$fetchIngredients$0(RestExamples.java:34) ~[classes!/:0.0.7-SNAPSHOT]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:767) ~[spring-boot-3.0.0.jar!/:3.0.0]
11 common frames omitted
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: JSON解析错误:无法将对象值反序列化为类型[Lru.sandwichcloud.Ingredient;(tokenJsonToken.START_OBJECT)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:406) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:354) ~[spring-web-6.0.2.jar!/:6.0.2]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:103) ~[spring-web-6.0.2.jar!/:6.0.2]
19 common frames omitted
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: 无法将对象值反序列化为类型[Lru.sandwichcloud.Ingredient;(tokenJsonToken.START_OBJECT)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1746) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1520) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.DeserializationContext.handleUnexpectedToken(DeserializationContext.java:1467) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.handleNonArray(ObjectArrayDeserializer.java:345) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:196) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.deser.std.ObjectArrayDeserializer.deserialize(ObjectArrayDeserializer.java:26) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2105) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1481) ~[jackson-databind-2.14.1.jar!/:2.14.1]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:395) ~[spring-web-6.0.2.jar!/:6.0.2]
21 common frames omitted
批量接口返回的JSON结构
{ "_embedded" : { "ingredients" : [ { "name" : "Dark Bread", "type" : "BREAD", "_links" : { "self" : { "href" : "http://localhost:8080/data-api/ingredients/DARK" }, "ingredient" : { "href" : "http://localhost:8080/data-api/ingredients/DARK" } } }, { "name" : "White Bread", "type" : "BREAD", "_links" : { "self" : { "href" : "http://localhost:8080/data-api/ingredients/WHTE" }, "ingredient" : { "href" : "http://localhost:8080/data-api/ingredients/WHTE" } } }, { "name" : "Beef", "type" : "PROTEIN", "_links" : { "self" : { "href" : "http://localhost:8080/data-api/ingredients/BEEF" }, "ingredient" : { "href" : "http://localhost:8080/data-api/ingredients/BEEF" } } }, { "name" : "Mustard", "type" : "SAUCE", "_links" : { "self" : { "href" : "http://localhost:8080/data-api/ingredients/MTRD" }, "ingredient" : { "href" : "http://localhost:8080/data-api/ingredients/MTRD" } } } ] }, "_links" : { "self" : { "href" : "http://localhost:8080/data-api/ingredients" }, "profile" : { "href" : "http://localhost:8080/data-api/profile/ingredients" } } }
相关代码
批量获取列表的代码(出错版本)
public List<Ingredient> getAllIngredients() { return rest.exchange("http://localhost:8080/data-api/ingredients", HttpMethod.GET, null, new ParameterizedTypeReference<List<Ingredient>>() {} ).getBody(); }
Ingredient实体类
@Data @RequiredArgsConstructor @NoArgsConstructor(access=AccessLevel.PRIVATE, force=true) @Entity public class Ingredient { @Id private final String id; private final String name; private final Type type; public enum Type { BREAD, PROTEIN, VEGGIES, CHEESE, SAUCE } }
单个对象获取代码(正常运行)
public Ingredient getIngredientById(String ingredientId) { return rest.getForObject( "http://localhost:8080/data-api/ingredients/{id}", Ingredient.class, ingredientId); }
错误原因
批量接口返回的是HAL(Hypertext Application Language)格式的JSON,最外层是一个包含_embedded和_links的对象,Ingredient列表嵌套在_embedded.ingredients路径下。而代码直接尝试将整个响应反序列化为List<Ingredient>,Jackson无法将外层对象转换成列表,因此抛出类型不匹配的反序列化错误。
单个对象接口可以正常工作,是因为其返回的JSON结构直接包含name、type等Ingredient类的字段,Jackson默认会忽略未知的_links字段,所以能成功反序列化。
解决方案
方案1:创建HAL格式的包装类(推荐)
定义一个包装类来匹配批量接口的JSON结构,再从中提取Ingredient列表:
@Data public class IngredientWrapper { private Embedded _embedded; @Data public static class Embedded { private List<Ingredient> ingredients; } }
修改批量获取方法:
public List<Ingredient> getAllIngredients() { ResponseEntity<IngredientWrapper> response = rest.exchange( "http://localhost:8080/data-api/ingredients", HttpMethod.GET, null, IngredientWrapper.class ); return response.getBody().get_embedded().getIngredients(); }
方案2:使用JsonNode手动解析(无需新增类)
如果不想创建包装类,可以先将响应读取为字符串,再通过Jackson的JsonNode解析提取列表:
public List<Ingredient> getAllIngredients() { ResponseEntity<String> response = rest.exchange( "http://localhost:8080/data-api/ingredients", HttpMethod.GET, null, String.class ); ObjectMapper mapper = new ObjectMapper(); try { JsonNode root = mapper.readTree(response.getBody()); JsonNode ingredientsNode = root.path("_embedded").path("ingredients"); return mapper.readValue(ingredientsNode.toString(), new TypeReference<List<Ingredient>>() {}); } catch (IOException e) { throw new RuntimeException("解析Ingredient列表失败", e); } }
内容的提问来源于stack exchange,提问作者tsavaph

