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

2022-09-03 02:07:41

我有一个与后端微服务通信的 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;
    }
}

这将生成根据本文顶部的API规范格式化的响应。这种方法的好处是,我们只公开那些我们想要公开的字段。沉重的缺点是,我有大量的包装类四处飞来飞去,除了被Jackson读取以产生正确格式的响应之外,它们不执行任何可识别的逻辑任务。JSON

我的问题如下:

  • 我怎么可能推广这种方法?理想情况下,我希望有一个我的所有模型都可以扩展的单个类(也许是一个类),但是看到Jackson似乎如何从对象定义中派生JSON键,我必须使用一些类似于在运行时向基本响应类添加字段的东西 - 这是一个肮脏的黑客,我想尽可能远离人类。BaseSingularResponseBaseCollectionsResponse extends ResourceSupportJavaassist

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

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


答案 1

有几种可能性。

您可以使用 :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)

答案 2

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

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

如果您不需要反序列化响应,则可以创建一个通用包装器和一个自定义的 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,该配置通过覆盖来扩展。请注意,这样做会禁用转换器的默认自动检测,因此您可能必须自己添加默认值(检查Spring源代码以查看默认值。如果您扩展,则可以直接调用(就个人而言,如果我需要自定义任何内容,我更喜欢使用,但是这样做有一些小影响, 您可以在其他文章中阅读。WebMvcConfigurerAdapterconfigureMessageConverters()WebMvcConfigurationSupport#addDefaultHttpMessageConverters()WebMvcConfigurationSupportWebMvcConfigurerAdapteraddDefaultHttpMessageConvertersWebMvcConfigurationSupportWebMvcConfigurerAdapter