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

如何使用RestTemplate实现JSON响应映射中外层类的泛化?

泛化处理WebService返回的统一结构JSON

我正在开发一系列WebService客户端,发现这些服务返回的JSON结构具有相似性,示例如下:

{
  "typex": { "property1" : "value"},
  "page": 1,
  "count": 200,
  "next_page": 2
}

共有近15个此类WebService,它们的返回结构一致:核心信息位于名为typex这类不同名称的对象中,外层包含page、count、next_page等字段。

目前我需要为每个服务创建至少两个类:一个外层包装类和一个内层信息类,示例代码如下:

public class TypeXWrapper {
 @JsonProperty
 TypeX typex;
 @JsonProperty
 Integer page;
 @JsonProperty
 Integer count;
 @JsonProperty
 Integer next_page;
}
public class TypeX {
 @JsonProperty
 String property1;
}

这样总共需要创建30个类。

有没有办法对此类场景实现泛化处理?

我曾设想如下实现方式:

public class GenericWrapper<InnerClass> {
 @JsonProperty
 InnerClass data;
 @JsonProperty
 Integer page;
 @JsonProperty
 Integer count;
 @JsonProperty
 Integer next_page;
}
public class TypeX {
 @JsonProperty
 String property1;
}

但问题在于,内层类对应的JSON根属性名会随WebService端点变化,比如TypeY对应的属性名为typey,示例如下:

{
  "typey": { "property2" : "value"},
  "page": 1,
  "count": 200,
  "next_page": 2
}

我的开发环境为Spring Boot 2.1.18(可按需升级),使用resttemplate并通过ParameterizedTypeReference封装返回对象。


解决方案

方案1:@JsonAlias+泛型包装类(适用于属性名有限的场景)

如果内层JSON属性名是固定的几个集合,可以直接给泛型包装类的data字段添加多个别名,让Jackson自动匹配:

public class GenericWrapper<T> {
    @JsonAlias({"typex", "typey", "typez"}) // 枚举所有可能的内层属性名
    @JsonProperty
    T data;
    @JsonProperty
    Integer page;
    @JsonProperty
    Integer count;
    @JsonProperty("next_page")
    Integer nextPage;

    // 省略getter/setter
}

使用时保持原有方式:

ParameterizedTypeReference<GenericWrapper<TypeX>> typeRef = new ParameterizedTypeReference<>() {};
GenericWrapper<TypeX> response = restTemplate.exchange(url, HttpMethod.GET, null, typeRef).getBody();

缺点:后续新增属性名时需要修改包装类,扩展性一般。

方案2:自定义注解+Jackson反序列化器(动态适配属性名)

这个方案通过自定义注解标记内层类对应的JSON属性名,配合反序列化器动态解析,扩展性强。

步骤1:定义自定义注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InnerJsonProperty {
    String value(); // 指定对应的JSON属性名
}

步骤2:编写泛型包装类

@JsonDeserialize(using = GenericWrapperDeserializer.class)
public class GenericWrapper<T> {
    T data;
    Integer page;
    Integer count;
    @JsonProperty("next_page")
    Integer nextPage;

    // 省略getter/setter
}

步骤3:实现自定义反序列化器

public class GenericWrapperDeserializer extends StdDeserializer<GenericWrapper<?>> {

    public GenericWrapperDeserializer() {
        super(GenericWrapper.class);
    }

    @Override
    public GenericWrapper<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        ObjectNode root = p.readValueAsTree();
        GenericWrapper<?> wrapper = new GenericWrapper<>();

        // 解析固定外层字段
        wrapper.setPage(root.get("page").asInt());
        wrapper.setCount(root.get("count").asInt());
        wrapper.setNextPage(root.get("next_page").asInt());

        // 获取泛型实际类型,读取注解中的属性名
        JavaType wrapperType = ctxt.getContextualType();
        JavaType innerType = wrapperType.getBindings().getBoundType(0);
        Class<?> innerClass = innerType.getRawClass();
        InnerJsonProperty annotation = innerClass.getAnnotation(InnerJsonProperty.class);
        if (annotation == null) {
            throw new IllegalArgumentException(innerClass.getName() + "必须添加@InnerJsonProperty注解");
        }
        String innerFieldName = annotation.value();

        // 解析内层数据
        JsonNode innerNode = root.get(innerFieldName);
        if (innerNode != null) {
            Object innerData = ctxt.readValue(innerNode.traverse(), innerType);
            wrapper.setData(innerData);
        }

        return wrapper;
    }
}

步骤4:给内层类添加注解

@InnerJsonProperty("typex")
public class TypeX {
    @JsonProperty
    String property1;
    // 省略getter/setter
}

@InnerJsonProperty("typey")
public class TypeY {
    @JsonProperty
    String property2;
    // 省略getter/setter
}

步骤5:使用方式不变

ParameterizedTypeReference<GenericWrapper<TypeX>> typeRef = new ParameterizedTypeReference<>() {};
GenericWrapper<TypeX> response = restTemplate.exchange(url, HttpMethod.GET, null, typeRef).getBody();

方案3:扩展RestTemplate消息转换器(无侵入式适配)

如果不想修改实体类,可以在消息转换器层面动态替换JSON属性名,把动态字段名映射为包装类的data字段。

步骤1:自定义Jackson消息转换器

public class DynamicPropertyNameConverter extends MappingJackson2HttpMessageConverter {

    private final Map<Class<?>, String> innerClassToFieldMap;

    public DynamicPropertyNameConverter(Map<Class<?>, String> innerClassToFieldMap) {
        this.innerClassToFieldMap = innerClassToFieldMap;
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new DynamicPropertyModule(innerClassToFieldMap));
        setObjectMapper(objectMapper);
    }

    private static class DynamicPropertyModule extends SimpleModule {
        private final Map<Class<?>, String> innerClassToFieldMap;

        public DynamicPropertyModule(Map<Class<?>, String> innerClassToFieldMap) {
            this.innerClassToFieldMap = innerClassToFieldMap;
        }

        @Override
        public void setupModule(SetupContext context) {
            super.setupModule(context);
            context.addBeanDeserializerModifier(new BeanDeserializerModifier() {
                @Override
                public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
                    if (GenericWrapper.class.isAssignableFrom(beanDesc.getBeanClass())) {
                        return new GenericWrapperDynamicDeserializer((JsonDeserializer<Object>) deserializer, innerClassToFieldMap);
                    }
                    return deserializer;
                }
            });
        }
    }

    private static class GenericWrapperDynamicDeserializer extends DelegatingDeserializer {
        private final Map<Class<?>, String> innerClassToFieldMap;

        public GenericWrapperDynamicDeserializer(JsonDeserializer<Object> delegate, Map<Class<?>, String> innerClassToFieldMap) {
            super(delegate);
            this.innerClassToFieldMap = innerClassToFieldMap;
        }

        @Override
        protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegate) {
            return new GenericWrapperDynamicDeserializer((JsonDeserializer<Object>) newDelegate, innerClassToFieldMap);
        }

        @Override
        public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
            ObjectNode root = p.readValueAsTree();
            // 获取泛型类型,匹配对应的属性名
            JavaType wrapperType = ctxt.getContextualType();
            JavaType innerType = wrapperType.getBindings().getBoundType(0);
            Class<?> innerClass = innerType.getRawClass();
            String fieldName = innerClassToFieldMap.get(innerClass);
            if (fieldName == null) {
                throw new IllegalArgumentException("未配置" + innerClass.getName() + "对应的JSON属性名");
            }
            // 替换属性名为固定的"data"
            JsonNode innerNode = root.get(fieldName);
            root.remove(fieldName);
            root.set("data", innerNode);
            // 继续反序列化
            return super.deserialize(root.traverse(), ctxt);
        }
    }
}

步骤2:配置RestTemplate

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 配置内层类与JSON属性名的映射
        Map<Class<?>, String> fieldMap = new HashMap<>();
        fieldMap.put(TypeX.class, "typex");
        fieldMap.put(TypeY.class, "typey");
        // 新增服务时只需添加此处映射
        DynamicPropertyNameConverter converter = new DynamicPropertyNameConverter(fieldMap);
        // 替换默认的Jackson转换器
        restTemplate.getMessageConverters().removeIf(c -> c instanceof MappingJackson2HttpMessageConverter);
        restTemplate.getMessageConverters().add(converter);
        return restTemplate;
    }
}

步骤3:使用泛型包装类

public class GenericWrapper<T> {
    @JsonProperty
    T data;
    @JsonProperty
    Integer page;
    @JsonProperty
    Integer count;
    @JsonProperty("next_page")
    Integer nextPage;

    // 省略getter/setter
}

使用方式和原有逻辑完全一致,无需修改实体类。


内容的提问来源于stack exchange,提问作者fsilva

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 19:31:22