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



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

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

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 规范进行格式化。这种方法的好处是,我们只公开那些我们想要公开的字段。严重的缺点是,我有大量的包装类,除了被杰克逊读取以产生正确格式的响应之外,它们不执行任何可识别的逻辑任务。

我的问题如下:

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

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

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

有几种可能性。

您可以使用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());
}
}

您还可以在类级别中使用杰克逊批注@JsonTypeInfo

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

就个人而言,我不介意额外的 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。请注意,这样做会禁用转换器的默认自动检测,因此您可能必须自己添加默认值(检查 Spring 源代码以获取WebMvcConfigurationSupport#addDefaultHttpMessageConverters()以查看默认值。 如果您扩展WebMvcConfigurationSupport而不是WebMvcConfigurerAdapter您可以直接调用addDefaultHttpMessageConverters(就个人而言,如果我需要自定义任何内容,我更喜欢使用WebMvcConfigurationSupport而不是WebMvcConfigurerAdapter, 但是这样做有一些小的影响,你可能会在其他文章中读到。

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

我猜你正在寻找自定义杰克逊序列化程序。通过简单的代码实现,可以在不同的结构中序列化相同的对象

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

最新更新