【问题标题】:Jackson's @JsonView, @JsonFilter and SpringJackson 的@JsonView、@JsonFilter 和 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 控制器方法上指定这一点,因为调用的方法定义了客户端请求数据的上下文。

【问题讨论】:

标签: java json spring spring-mvc jackson


【解决方案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 如果性能很重要,这个项目还有一个更简单的替代方案。
      • 谢谢!我会检查这个-
      猜你喜欢
      • 1970-01-01
      • 2019-02-27
      • 1970-01-01
      • 2015-10-30
      • 2016-11-13
      • 1970-01-01
      • 2014-10-23
      • 1970-01-01
      • 2020-12-22
      相关资源
      最近更新 更多