【问题标题】:Spring Boot: Wrapping JSON response in dynamic parent objectsSpring Boot:将 JSON 响应包装在动态父对象中
【发布时间】: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 的东西吗?

【问题讨论】:

    标签: java json spring spring-boot jackson


    【解决方案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】:

        【讨论】:

          猜你喜欢
          • 2014-10-12
          • 2017-12-04
          • 1970-01-01
          • 2014-07-27
          • 1970-01-01
          • 1970-01-01
          • 2016-02-25
          • 2020-12-07
          • 1970-01-01
          相关资源
          最近更新 更多