Jackson with Spring boot:控制对象替换特定方法

Posted

技术标签:

【中文标题】Jackson with Spring boot:控制对象替换特定方法【英文标题】:Jackson with Spring boot: control object substitution for specific method 【发布时间】:2018-09-23 07:55:13 【问题描述】:

我将实体类型作为递归树中的项,因此任何项都引用其父项和子项(相同类型)

public class Category 
  private Integer id;
  private String displayName;

  @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
  @JsonIdentityReference(alwaysAsId=true)
  private Category parent;

  @JsonInclude(JsonInclude.Include.NON_EMPTY)
  @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="id")
  @JsonIdentityReference(alwaysAsId=true)
  private Set<Category> children;

  // constructors, getters and setters

如您所见,我用@JsonIdentityReference 注释标记了两个引用字段,强制它们呈现为普通ID。目前使用此设置,实体呈现如下:

// from .../categories/0

  "id" : 0,
  "displayName" : "Root",
  "parent" : null,
  "children" : [ 1, 13, 17 ]

(这很好)。

然而,客户端非常常见的使用场景是从特定节点开始获取整个子树,在此实现中需要向服务器发出多个请求。我想创建另一个 enpoint,允许客户端通过单个请求执行此操作。

@RestController
@RequestMapping(value = "/categories")
public class CategoryController 
  @Autowired
  private CategoryService categoryService;
  @Autowired
  private CategoryRepository categoryRepo;

  @RequestMapping(value = "/tree", method = RequestMethod.GET)
  public Category getTree(@RequestParam(name = "root", required = false) Integer id) 
    Category root = id == null ? categoryService.getRoot() : categoryRepo.findOne(id);
    return categoryService.getTree(root);
  

  // other endpoints
  // getOne(id)
  // getAll()

仅当我从 children 字段中手动删除 (alwaysAsId=true) 标志时,来自此端点的响应才会呈现完整对象。但是,我希望两个端点共存并呈现不同的布局。所以,问题是:如何让特定的控制器方法选择是否用 id 替换完整的实体?


我已经尝试过与@JsonView 的各种组合,但似乎这种方法不起作用。 @JsonView 只能控制是否包含或完全省略特定字段,而我需要 children 字段仅更改布局。另外,由于子类型与实体类型相同,因此无法在不同类之间拆分注释标记。

【问题讨论】:

我知道我已经提交了一个关于此的功能请求,但我找不到它。 【参考方案1】:

/* 更新了答案,在关于失去多层次层次结构中的视图标记的评论之后。 */

您仍然可以通过使用@JsonViews 和在Category 对象内的小变通方法来实现您正在寻找的结果。

假设我们有两种类型用作标记来输出 JSON 视图 FlatViewHierarchicalView

解决的原理是:

我们将大部分字段与视图相关联。 children 字段仅与 FlatView 关联。

我们为children 字段创建了一个额外的getter,为其提供了另一个属性名称,不会与该字段或其原始getter 发生冲突。此属性仅与 HierarchicalView 关联。

它为Category 类提供以下布局:

public class Category 

  @JsonView( FlatView.class, HierarchicalView.class) // both views
  private Integer id;

  @JsonView( FlatView.class, HierarchicalView.class) // both views
  private String displayName;

  @JsonView( FlatView.class, HierarchicalView.class) // both views
  @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
  @JsonIdentityReference(alwaysAsId = true)
  private Category parent;

  @JsonView(FlatView.class) // flat view only!
  @JsonInclude(JsonInclude.Include.NON_EMPTY)
  @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
  @JsonIdentityReference(alwaysAsId = true)
  private Set<Category> children;

  @JsonView(HierarchicalView.class) // hierarchical view only!
  // note that the name is not `getChildren`: this generates another JSON property. 
  // Or use @JsonProperty to customize it.
  public Set<Category> getChildrenAsTree()  
    return this.children;
  

  // constructors, getters and setters

产生输出的控制器方法应该用各自的@JsonViews 注释。

缺点:

此方法不会对以不同方式表示的相同信息使用相同的属性名称。这可能会给客户端的属性匹配、反序列化带来一些困难。

如果Category 在某处仍使用默认视图,它将包含通过附加getter 访问的字段和属性。

冗余和重复的注释 :) 更难维护。

children 字段的冗余 getter。

【讨论】:

我尝试了这个想法,但它有一个严重的缺点 - 当jsonGenerator.writeObject() 被调用时,上下文丢失并且构造了新的SerializerProvider 不知道活动视图。结果,只有树的第一层被渲染,更深的层落入另一个分支。应该明确使用DefaultSerializerProvider(不知何故),我仍在尝试找出确切的方法。 好话,它让我重新思考了这种方法。我已经更新了答案。【参考方案2】:

我终于找到了解决办法。它基于 Antot 答案的第一个版本,并为 children 集合的 content 使用自定义序列化程序。生成树的端点用@JsonView(Category.TreeView.class) 注释标记,未标记的字段包含打开

#application.properties
spring.jackson.mapper.default-view-inclusion=true

// Category.java
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonSerialize(contentUsing = CategoryChildSerializer.class)
private Set<Category> children;

// CategoryChildSerializer.java
public class CategoryChildSerializer extends JsonSerializer<Category> implements ResolvableSerializer 
  private JsonSerializer<Object> defaultSerializer;
  private JsonSerializer<Object> idSerializer;

  public void serialize(Category value, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException 
    if(provider.getActiveView() == Category.TreeView.class)
      defaultSerializer.serialize(value, gen, provider);
    else
      idSerializer.serialize(value.getId(), gen, provider);
  

  public void resolve(SerializerProvider provider) throws JsonMappingException 
    defaultSerializer = provider.findValueSerializer(Category.class);
    idSerializer = provider.findValueSerializer(Integer.class);
  

注意切换序列化器如何委托序列化而不是直接使用JsonGenerator。实现ResolvableSerializer 实际上是可选的,它只是优化步骤。

【讨论】:

以上是关于Jackson with Spring boot:控制对象替换特定方法的主要内容,如果未能解决你的问题,请参考以下文章

spring-boot 使用啥版本的 Jackson?

Jackson 在我的 Spring Boot 应用程序中忽略了 spring.jackson.properties

如何在 Spring Boot 1.4 中自定义 Jackson

Spring Boot没有使用Jackson Kotlin插件

Spring Boot 未使用 Jackson Kotlin 插件

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