关于java:Spring Boot:在动态父对象中包装JSON响应

Spring Boot: Wrapping JSON response in dynamic parent objects

我有一个与后端微服务进行对话的RESTAPI规范,它返回以下值:

关于"集合"响应(例如get/users):

1
2
3
4
5
6
7
8
9
10
11
12
{
    users: [
        {
            ... // single user object data
        }
    ],
    links: [
        {
            ... // single HATEOAS link object
        }
    ]
}

关于"单目标"响应(如GET /users/{userUuid}):

1
2
3
4
5
{
    user: {
        ... // {userUuid} user object}
    }
}

选择这种方法是为了使单个响应具有可扩展性(例如,如果GET /users/{userUuid}沿着行获得一个额外的查询参数,例如在?detailedView=true处,我们将获得额外的请求信息)。

从根本上讲,我认为这是一种最小化API更新之间中断更改的好方法。然而,将这个模型转换为代码是非常困难的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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,但不能公开其他的。然后,对于集合响应,我将拥有以下包装类:

1
2
3
4
5
6
7
8
9
public class UsersResource extends ResourceSupport {

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

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

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

1
2
3
4
5
6
7
8
9
public class UserResource {

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

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

这将生成JSON响应,这些响应按照本文顶部的API规范进行格式化。这种方法的好处是我们只公开那些我们想要公开的字段。严重的缺点是我有大量的包装类在运行,除了被Jackson读取以生成正确格式的响应之外,这些类没有执行任何可识别的逻辑任务。

我的问题如下:

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

  • 有没有更简单的方法来完成这个?不幸的是,一年后的响应中可能会有数量可变的顶级JSON对象,因此我不能使用像Jackson的SerializationConfig.Feature.WRAP_ROOT_VALUE这样的东西,因为它将所有东西都包装成一个根级别的对象(据我所知)。

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


有几种可能性。

您可以使用java.util.Map

1
2
3
4
5
6
7
8
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来包装您可以使用的响应,如下所示:

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

这里有一个推广这个系列化的建议。

处理简单对象的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
    }
}

要处理集合的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
    }
}

以及一个示例应用程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

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


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

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

如果不需要反序列化响应,则可以创建通用包装器和自定义httpmessageconverter(并将其放在消息转换器列表中映射jackson2httpmessageconverter之前)。这样地:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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,但是这样做有一些小的影响,你可能会在其他文章中看到。


我想你在找定制的杰克逊序列化程序。使用简单的代码实现,相同的对象可以在不同的结构中序列化。

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


Jackson不太支持动态/变量JSON结构,所以任何实现这种功能的解决方案都会非常糟糕,正如您所提到的。据我所知,从我所看到的来看,标准和最常见的方法是使用像您目前这样的包装类。包装器类确实是组合在一起的,但是如果您对自己的固有能力有创造性,您可能会发现类之间的一些共性,从而减少包装器类的数量。否则,您可能需要编写一个自定义框架。