Spring Boot:将 JSON 响应包装在动态父对象中

Posted

技术标签:

【中文标题】Spring Boot:将 JSON 响应包装在动态父对象中【英文标题】:Spring Boot: Wrapping JSON response in dynamic parent objects 【发布时间】:2017-06-12 08:15:25 【问题描述】:

我有一个与后端微服务通信的 REST API 规范,它返回以下值:

关于“收藏”响应(例如 GET /users):


    users: [
        
            ... // single user object data
        
    ],
    links: [
        
            ... // single HATEOAS link object
        
    ]

关于“单一对象”响应(例如GET /users/userUuid):


    user: 
        ... // userUuid user object
    

选择这种方法是为了使单个响应可以扩展(例如,如果GET /users/userUuid 得到一个额外的查询参数,例如?detailedView=true,我们就会有额外的请求信息)。

从根本上说,我认为这是一种可以最大限度地减少 API 更新之间的重大更改的方法。然而,将这个模型转化为代码非常困难。

假设对于单个响应,我为单个用户提供以下 API 模型对象:

public class SingleUserResource 
    private MicroserviceUserModel user;

    public SingleUserResource(MicroserviceUserModel user) 
        this.user = user;
    

    public String getName() 
        return user.getName();
    

    // other getters for fields we wish to expose

这种方法的优点是我们可以公开内部使用的模型中的字段,我们有公共getter,但不能公开其他字段。然后,对于集合响应,我将有以下包装类:

public class UsersResource extends ResourceSupport 

    @JsonProperty("users")
    public final List<SingleUserResource> users;

    public UsersResource(List<MicroserviceUserModel> users) 
        // add each user as a SingleUserResource
    

对于单个对象响应,我们将有以下内容:

public class UserResource 

    @JsonProperty("user")
    public final SingleUserResource user;

    public UserResource(SingleUserResource user) 
        this.user = user;
    

这会产生JSON 响应,其格式符合本文顶部的 API 规范。这种方法的好处是我们只公开那些我们想要公开的字段。严重的缺点是我有一个 ton 的包装类飞来飞去,除了被 Jackson 读取以产生正确格式的响应之外,它们没有执行任何可辨别的逻辑任务。

我的问题如下:

我怎样才能概括这种方法?理想情况下,我希望有一个 BaseSingularResponse 类(可能还有一个 BaseCollectionsResponse extends ResourceSupport 类),我的所有模型都可以扩展,但是看到 Jackson 似乎如何从对象定义中派生 JSON 键,我将不得不使用一些东西像 Javaassist 一样在运行时向基本响应类添加字段 - 我希望尽可能远离人类这种肮脏的黑客行为。

有没有更简单的方法来实现这一点?不幸的是,一年后我的响应中可能会有数量不定的*** JSON 对象,所以我不能使用像 Jackson 的 SerializationConfig.Feature.WRAP_ROOT_VALUE 这样的东西,因为它会将 everything 包装到单个根级对象中(据我所知)。

对于类级别(而不仅仅是方法和字段级别)可能有类似@JsonProperty 的东西吗?

【问题讨论】:

【参考方案1】:

Jackson 对动态/可变 JSON 结构的支持并不多,因此任何完成此类事情的解决方案都会像您提到的那样非常笨拙。据我所知和所见,标准和最常见的方法是像您目前一样使用包装类。包装类确实加起来了,但是如果你对你的继承有创意,你可能会发现类之间的一些共性,从而减少包装类的数量。否则,您可能会考虑编写自定义框架。

【讨论】:

【参考方案2】:

有几种可能。

您可以使用java.util.Map

List<UserResource> userResources = new ArrayList<>();
userResources.add(new UserResource("John"));
userResources.add(new UserResource("Jane"));
userResources.add(new UserResource("Martin"));
Map<String, List<UserResource>> usersMap = new HashMap<String, List<UserResource>>();
usersMap.put("users", userResources);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(usersMap));

您可以使用ObjectWriter 来包装您可以使用的响应,如下所示:

ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
result = writer.writeValueAsString(object);

这是一个概括这种序列化的命题。

处理简单对象的类

public abstract class BaseSingularResponse 

    private String root;

    protected BaseSingularResponse(String rootName) 
        this.root = rootName;
    

    public String serialize() 
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try 
            result = writer.writeValueAsString(this);
         catch (JsonProcessingException e) 
            result = e.getMessage();
        
        return result;
    

处理收集的类

public abstract class BaseCollectionsResponse<T extends Collection<?>> 
    private String root;
    private T collection;

    protected BaseCollectionsResponse(String rootName, T aCollection) 
        this.root = rootName;
        this.collection = aCollection;
    

    public T getCollection() 
        return collection;
    

    public String serialize() 
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try 
            result = writer.writeValueAsString(collection);
         catch (JsonProcessingException e) 
            result = e.getMessage();
        
        return result;
    

还有一个示例应用程序

public class Main 

    private static class UsersResource extends BaseCollectionsResponse<ArrayList<UserResource>> 
        public UsersResource() 
            super("users", new ArrayList<UserResource>());
        
    

    private static class UserResource extends BaseSingularResponse 

        private String name;
        private String id = UUID.randomUUID().toString();

        public UserResource(String userName) 
            super("user");
            this.name = userName;
        

        public String getUserName() 
            return this.name;
        

        public String getUserId() 
            return this.id;
        
    

    public static void main(String[] args) throws JsonProcessingException 
        UsersResource userCollection = new UsersResource();
        UserResource user1 = new UserResource("John");
        UserResource user2 = new UserResource("Jane");
        UserResource user3 = new UserResource("Martin");

        System.out.println(user1.serialize());

        userCollection.getCollection().add(user1);
        userCollection.getCollection().add(user2);
        userCollection.getCollection().add(user3);

        System.out.println(userCollection.serialize());
    


你也可以在类级别使用Jackson注解@JsonTypeInfo

@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)

【讨论】:

如果你在你的对象内部进行序列化,你错过了关于 Spring MVC 的一点。序列化被限制在 HttpMessageConverter 层。 我不得不说我的回答中没有考虑到 Spring MVC。但是,我认为建议的解决方案可以很容易地适应在 HtppMessageConverter 中使用。【参考方案3】:

我个人不介意额外的 Dto 类,您只需要创建一次,而且几乎没有维护成本。如果您需要进行 MockMVC 测试,您很可能需要这些类来反序列化您的 JSON 响应以验证结果。

您可能知道 Spring 框架在 HttpMessageConverter 层中处理对象的序列化/反序列化,因此这是更改对象序列化方式的正确位置。

如果您不需要反序列化响应,则可以创建一个通用包装器和一个自定义 HttpMessageConverter(并将其放在消息转换器列表中的 MappingJackson2HttpMessageConverter 之前)。像这样:

public class JSONWrapper 

    public final String name;
    public final Object object;

    public JSONWrapper(String name, Object object) 
        this.name = name;
        this.object = object;
    



public class JSONWrapperHttpMessageConverter extends MappingJackson2HttpMessageConverter 

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException 
        // cast is safe because this is only called when supports return true.
        JSONWrapper wrapper = (JSONWrapper) object;
        Map<String, Object> map = new HashMap<>();
        map.put(wrapper.name, wrapper.object);
        super.writeInternal(map, type, outputMessage);
    

    @Override
    protected boolean supports(Class<?> clazz) 
        return clazz.equals(JSONWrapper.class);
    

然后,您需要在 spring 配置中注册自定义 HttpMessageConverter,该配置通过覆盖 configureMessageConverters() 扩展 WebMvcConfigurerAdapter。请注意,这样做会禁用转换器的默认自动检测,因此您可能必须自己添加默认值(检查 WebMvcConfigurationSupport#addDefaultHttpMessageConverters() 的 Spring 源代码以查看默认值。如果您扩展 WebMvcConfigurationSupport 而不是 WebMvcConfigurerAdapter 您可以调用直接addDefaultHttpMessageConverters(如果我需要自定义任何内容,我个人更喜欢使用WebMvcConfigurationSupport 而不是WebMvcConfigurerAdapter,但是这样做有一些小影响,您可能可以在其他文章中阅读。

【讨论】:

【参考方案4】:

我猜你正在寻找Custom Jackson Serializer。通过简单的代码实现,相同的对象可以以不同的结构序列化

一些例子: https://***.com/a/10835504/814304 http://www.davismol.net/2015/05/18/jackson-create-and-register-a-custom-json-serializer-with-stdserializer-and-simplemodule-classes/

【讨论】:

以上是关于Spring Boot:将 JSON 响应包装在动态父对象中的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot - 将 JSON 转换为 JAVA

使用 thymeleaf 在浏览器中打印 Spring Boot JSON RESTful 响应

返回 JSON 作为响应 Spring Boot

spring-boot用jackson改变json响应结构

json响应spring boot中缺少字段

如何在 Spring Boot 中返回 JSON 响应?