Jackson 的@JsonView、@JsonFilter 和 Spring

Posted

技术标签:

【中文标题】Jackson 的@JsonView、@JsonFilter 和 Spring【英文标题】:Jackson's @JsonView, @JsonFilter and Spring 【发布时间】:2011-11-30 01:11:30 【问题描述】:

是否可以使用 Jackson @JsonView@JsonFilter 注释来修改 Spring MVC 控制器返回的 JSON,同时使用 MappingJacksonHttpMessageConverter 和 Spring 的 @ResponseBody@RequestBody 注释?

public class Product

    private Integer id;
    private Set<ProductDescription> descriptions;
    private BigDecimal price;
    ...



public class ProductDescription

    private Integer id;
    private Language language;
    private String name;
    private String summary;
    private String lifeStory;
    ...

当客户端请求Products 的集合时,我想返回每个ProductDescription 的最小版本,也许只是它的ID。然后在随后的调用中,客户端可以使用此 ID 请求具有所有属性的 ProductDescription 的完整实例。

最好能够在 Spring MVC 控制器方法上指定这一点,因为调用的方法定义了客户端请求数据的上下文。

【问题讨论】:

我有同样的问题。@StaxMan 回答有帮助,但没有回答。Jackson 库现在可能不支持这个。 这也是相关的***.com/questions/5772304/… 请在这里投票:jira.springsource.org/browse/… 【参考方案1】:

我不知道 Spring 是如何工作的(对不起!),但是 Jackson 1.9 可以使用 JAX-RS 方法中的 @JsonView 注释,所以你可以这样做:

@JsonView(ViewId.class)
@GET // and other JAX-RS annotations
public Pojo resourceMethod()

   return new Pojo();
 

Jackson 将使用 ViewId.class 标识的视图作为活动视图。也许 Spring 有(或将有)类似的能力?使用 JAX-RS,这是由标准的 JacksonJaxrsProvider 处理的,物有所值。

【讨论】:

【参考方案2】:

最终,我们希望使用类似于 StaxMan 为 JAX-RS 显示的符号。不幸的是,Spring 不支持这个开箱即用,所以我们必须自己做。

这是我的解决方案,它不是很漂亮,但确实可以。

@JsonView(ViewId.class)
@RequestMapping(value="get", method=RequestMethod.GET) // Spring controller annotation
public Pojo getPojo(@RequestValue Long id)

   return new Pojo(id);


public class JsonViewAwareJsonView extends MappingJacksonJsonView 

    private ObjectMapper objectMapper = new ObjectMapper();

    private boolean prefixJson = false;

    private JsonEncoding encoding = JsonEncoding.UTF8;

    @Override
    public void setPrefixJson(boolean prefixJson) 
        super.setPrefixJson(prefixJson);
        this.prefixJson = prefixJson;
    

    @Override
    public void setEncoding(JsonEncoding encoding) 
        super.setEncoding(encoding);
        this.encoding = encoding;
    


    @Override
    public void setObjectMapper(ObjectMapper objectMapper) 
        super.setObjectMapper(objectMapper);
        this.objectMapper = objectMapper;
    


    @Override
    protected void renderMergedOutputModel(Map<String, Object> model,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception 

        Class<?> jsonView = null;
        if(model.containsKey("json.JsonView"))
            Class<?>[] allJsonViews = (Class<?>[]) model.remove("json.JsonView");
            if(allJsonViews.length == 1)
                jsonView = allJsonViews[0];
        


        Object value = filterModel(model);
        JsonGenerator generator =
                this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding);
        if (this.prefixJson) 
            generator.writeRaw(" && ");
        
        if(jsonView != null)
            SerializationConfig config = this.objectMapper.getSerializationConfig();
            config = config.withView(jsonView);
            this.objectMapper.writeValue(generator, value, config);
        
        else
            this.objectMapper.writeValue(generator, value);
    


public class JsonViewInterceptor extends HandlerInterceptorAdapter


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler, ModelAndView modelAndView) 
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        JsonView jsonViewAnnotation = handlerMethod.getMethodAnnotation(JsonView.class);
        if(jsonViewAnnotation != null)
            modelAndView.addObject("json.JsonView", jsonViewAnnotation.value());
    


在 spring-servlet.xml 中

<bean name="ViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="mediaTypes">
            <map>
                <entry key="json" value="application/json" />
            </map>
        </property>
        <property name="defaultContentType" value="application/json" />
        <property name="defaultViews">
            <list>
                <bean class="com.mycompany.myproject.JsonViewAwareJsonView">
                </bean>
            </list>
        </property>
    </bean>

<mvc:interceptors>
    <bean class="com.mycompany.myproject.JsonViewInterceptor" />
</mvc:interceptors>

【讨论】:

你在哪里注册jsonViewInterceptor? 对不起,我现在添加了注册。【参考方案3】:

除了@user356083 之外,我还做了一些修改,以使这个示例在返回@ResponseBody 时能够正常工作。使用 ThreadLocal 有点 hack,但 Spring 似乎没有提供必要的上下文来以很好的方式做到这一点。

public class ViewThread  

    private static final ThreadLocal<Class<?>[]> viewThread = new ThreadLocal<Class<?>[]>(); 

    private static final Log log = LogFactory.getLog(SocialRequestUtils.class); 

    public static void setKey(Class<?>[] key) 
        viewThread.set(key); 
     

    public static Class<?>[] getKey() 
        if(viewThread.get() == null) 
            log.error("Missing threadLocale variable"); 

        return viewThread.get(); 
     
 

public class JsonViewInterceptor extends HandlerInterceptorAdapter  

    @Override 
    public boolean preHandle( 
            HttpServletRequest request, 
            HttpServletResponse response, 
            Object handler)  

        HandlerMethod handlerMethod = (HandlerMethod) handler; 

        JsonView jsonViewAnnotation = handlerMethod 
                .getMethodAnnotation(JsonView.class); 

        if (jsonViewAnnotation != null) 
            ViewThread.setKey(jsonViewAnnotation.value()); 

        return true; 
     
 

public class MappingJackson2HttpMessageConverter extends 
        AbstractHttpMessageConverter<Object>  

    @Override 
    protected void writeInternal(Object object, HttpOutputMessage outputMessage) 
            throws IOException, HttpMessageNotWritableException  

        JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); 
        JsonGenerator jsonGenerator = 
                this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding); 
        // This is a workaround for the fact JsonGenerators created by ObjectMapper#getJsonFactory 
        // do not have ObjectMapper serialization features applied. 
        // See https://github.com/FasterXML/jackson-databind/issues/12 
        if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT))  
            jsonGenerator.useDefaultPrettyPrinter(); 
         

        //A bit of a hack.  
        Class<?>[] jsonViews = ViewThread.getKey(); 

        ObjectWriter writer = null; 

        if(jsonViews != null) 
            writer = this.objectMapper.writerWithView(jsonViews[0]); 
        else 
            writer = this.objectMapper.writer(); 
         

        try  
            if (this.prefixJson)  
                jsonGenerator.writeRaw(" && "); 
             

            writer.writeValue(jsonGenerator, object); 

         
        catch (JsonProcessingException ex)  
            throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); 
         
    

【讨论】:

【参考方案4】:

为了寻找相同的答案,我想出了一个用视图包装 ResponseBody 对象的想法。

控制器类:

@RequestMapping(value="/id", headers="Accept=application/json", method= RequestMethod.GET)
     public  @ResponseBody ResponseBodyWrapper getCompany(HttpServletResponse response, @PathVariable Long id)
        ResponseBodyWrapper responseBody =  new ResponseBodyWrapper(companyService.get(id),Views.Owner.class);
        return responseBody;
     

public class ResponseBodyWrapper 
private Object object;
private Class<?> view;

public ResponseBodyWrapper(Object object, Class<?> view) 
    this.object = object;
    this.view = view;


public Object getObject() 
    return object;

public void setObject(Object object) 
    this.object = object;

@JsonIgnore
public Class<?> getView() 
    return view;

@JsonIgnore
public void setView(Class<?> view) 
    this.view = view;


然后我重写writeInternal 方法形式MappingJackson2HttpMessageConverter 来检查要序列化的对象是否是instanceof 包装器,如果是,我用所需的视图序列化对象。

public class CustomMappingJackson2 extends MappingJackson2HttpMessageConverter 

private ObjectMapper objectMapper = new ObjectMapper();
private boolean prefixJson;

@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotWritableException 

    JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
    JsonGenerator jsonGenerator =
            this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
    try 
        if (this.prefixJson) 
            jsonGenerator.writeRaw(" && ");
        
        if(object instanceof ResponseBodyWrapper)
            ResponseBodyWrapper responseBody = (ResponseBodyWrapper) object;
            this.objectMapper.writerWithView(responseBody.getView()).writeValue(jsonGenerator, responseBody.getObject());
        else
            this.objectMapper.writeValue(jsonGenerator, object);
        
    
    catch (IOException ex) 
        throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
    


public void setObjectMapper(ObjectMapper objectMapper) 
    Assert.notNull(objectMapper, "ObjectMapper must not be null");
    this.objectMapper = objectMapper;
    super.setObjectMapper(objectMapper);


public ObjectMapper getObjectMapper() 
    return this.objectMapper;


public void setPrefixJson(boolean prefixJson) 
    this.prefixJson = prefixJson;
    super.setPrefixJson(prefixJson);

【讨论】:

非常好的方法。对于 Web 服务,使用转换器比使用视图更干净(尽管您当然可以使用任何一种)。您的 ResponseBodyWrapper 是一种将 JSON 视图传达给转换器的创造性方式。 唯一轻微的遗憾是控制器必须返回一些不是要序列化的对象的东西。换句话说,它与客户转换器有些紧密的联系。在我的情况下,我可以忍受,但很高兴知道。 像魅力一样工作!感谢分享。【参考方案5】:

在经历了许多头撞死角和书呆子愤怒发脾气之后,这个问题的答案是...... 很简单。在这个用例中,我们有一个 Customer bean,其中嵌入了一个复杂的对象 Address,当发生 json 序列化时,我们希望防止地址中的属性名称 surburb and street 序列化。

我们通过在 Customer 类的 in 字段地址上应用注释 @JsonIgnoreProperties("suburb") 来做到这一点,要忽略的字段数量是无限的。例如,我想忽略郊区和街道。我会用 @JsonIgnoreProperties("suburb", "street")

注释地址字段

通过这样做,我们可以创建 HATEOAS 类型的架构。

下面是完整代码

客户.java

public class Customer 

private int id;
private String email;
private String name;

@JsonIgnoreProperties("suburb", "street")
private Address address;

public Address getAddress() 
    return address;


public void setAddress(Address address) 
    this.address = address;


public int getId() 
    return id;


public void setId(int id) 
    this.id = id;


public String getEmail() 
    return email;


public void setEmail(String email) 
    this.email = email;


public String getName() 
    return name;


public void setName(String name) 
    this.name = name;

地址.java 公共类地址

private String street;
private String suburb;
private String Link link;

public Link getLink() 
    return link;


public void setLink(Link link) 
    this.link = link;



public String getStreet() 
    return street;


public void setStreet(String street) 
    this.street = street;


public String getSuburb() 
    return suburb;


public void setSuburb(String suburb) 
    this.suburb = suburb;

【讨论】:

我认为@JsonIgnoreProperties 只能应用于类,不能应用于属性。你的代码不会为我编译。 这取决于您使用的新版本允许对字段进行注释的版本,请自行查看java doc。 fasterxml.github.io/jackson-annotations/javadoc/2.0.0/com/… 问题是您可能有不同的序列化配置文件,并且地址可能有返回客户的链接。 我认为这是针对这种特定情况的最佳解决方案。它帮助了我(比 JsonView 更好),而且我的情况与给定的情况非常相似【参考方案6】:

这个问题解决了! 关注this

添加对 Jackson 序列化视图的支持

Spring MVC 现在支持 Jackon 的序列化视图进行渲染 来自不同控制器的相同 POJO 的不同子集 方法(例如详细页面与摘要视图)。 问题:SPR-7156

这是SPR-7156。

状态:已解决

说明

Jackson 的 JSONView 注释允许开发人员控制方法的哪些方面被序列化。对于当前的实现,必须使用 Jackson 视图编写器,但内容类型不可用。如果作为 RequestBody 注解的一部分,可以指定 JSONView 会更好。

可在Spring ver >= 4.1

更新

关注此link。举例说明@JsonView 注解。

【讨论】:

有没有使用新功能的例子?【参考方案7】:

更好的是,从 4.2.0.Release 开始,您可以简单地执行以下操作:

@RequestMapping(method = RequestMethod.POST, value = "/springjsonfilter")
    public @ResponseBody MappingJacksonValue byJsonFilter(...) 
        MappingJacksonValue jacksonValue = new MappingJacksonValue(responseObj);    
        jacksonValue.setFilters(customFilterObj);
        return jacksonValue;
    

参考资料: 1.https://jira.spring.io/browse/SPR-12586 2.http://wiki.fasterxml.com/JacksonFeatureJsonFilter

【讨论】:

您能否详细说明这种方法或指出一个完整的示例?我同意这看起来是处理事情的正确方法- 这里有一个稍微详细一点的例子:github.com/krishna81m/jackson-nested-prop-filter 如果性能很重要,这个项目还有一个更简单的替代方案。 谢谢!我会检查这个-

以上是关于Jackson 的@JsonView、@JsonFilter 和 Spring的主要内容,如果未能解决你的问题,请参考以下文章

@JsonView 不过滤属性(Spring 4.1.0.RC2,Jackson 2.3.2)

杰克逊:忽略getter,但不使用@JsonView

css [JSONView Solarized Dark]用于JSONView扩展的Solarized Dark配色方案。 #solarized #jsonview

@JsonView

@JsonView的用法

我如何拥有多个JsonView继承