Spring and JacksonJson, serialising different fields with views

牧云@^-^@ 提交于 2019-12-20 12:31:44

问题


In a previous similar question, I asked about, how to serialise two different sets of fields using JacksonJson and Spring.

My use case is the typical Controller mapping with @ResponseBody annotation returning directly a particular object or collections of objects, that are then rendered with JacksonJson whenever the client adds application/json in the Accept header.

I had two answers, the first one suggests to return different interfaces with a different getter list, the second suggests to use Json Views.

I don't have problems to understand the first way, however, for the second, after reading the documentation on JacksonJsonViews, I don't know how to implement it with Spring.

To stay with the example, I would declare three stub classes, inside the class Views:

// View definitions:
public class Views {
    public static class Public { }
    public static class ExtendedPublic extends PublicView { }
    public static class Internal extends ExtendedPublicView { }
}

Then I've to declare the classes mentioned:

public class PublicView { }   
public class ExtendedPublicView { }

Why on earth they declare empty static classes and external empty classes, I don't know. I understand that they need a "label", but then the static members of Views would be enough. And it's not that ExtendedPublic extends Public, as it would be logical, but they are in fact totally unrelated.

And finally the bean will specify with annotation the view or list of views:

//changed other classes to String for simplicity and fixed typo 
//in classname, the values are hardcoded, just for testing
public class Bean {
    // Name is public
    @JsonView(Views.Public.class)
    String name = "just testing";

    // Address semi-public
    @JsonView(Views.ExtendedPublic.class)
    String address = "address";

    // SSN only for internal usage
    @JsonView(Views.Internal.class)
    String ssn = "32342342";
}

Finally in the Spring Controller, I've to think how to change the original mapping of my test bean:

@RequestMapping(value = "/bean") 
@ResponseBody
public final Bean getBean() {
    return new Bean();
}

It says to call:

//or, starting with 1.5, more convenient (ObjectWriter is reusable too)
objectMapper.viewWriter(ViewsPublic.class).writeValue(out, beanInstance);

So I have an ObjectMapper instance coming out of nowhere and an out which is not the servlet typical PrintWriter out = response.getWriter();, but is an instance of JsonGenerator and that can't be obtained with the new operator. So I don't know how to modify the method, here is an incomplete try:

@RequestMapping(value = "/bean")
@ResponseBody
public final Bean getBean() throws JsonGenerationException, JsonMappingException, IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    JsonGenerator out; //how to create?
    objectMapper.viewWriter(Views.Public.class).writeValue(out, new Bean());
    return ??; //what should I return?
}

So I would like to know if anybody had success using JsonView with Spring and how he/she did. The whole concept seems interesting, but the documentation seems lacking, also the example code is missing.

If it's not possible I will just use interfaces extending each others. Sorry for the long question.


回答1:


Based on the answers by @igbopie and @chrislovecnm, I've put together an annotation driven solution:

@Controller
public class BookService
{
    @RequestMapping("/books")
    @ResponseView(SummaryView.class)
    public @ResponseBody List<Book> getBookSummaries() {}

    @RequestMapping("/books/{bookId}")
    public @ResponseBody Book getBook(@PathVariable("bookId") Long BookId) {}
}

Where SummaryView is annotated on the Book model like so:

@Data
class Book extends BaseEntity
{
    @JsonView(SummaryView.class)
    private String title;
    @JsonView(SummaryView.class)
    private String author;
    private String review;

    public static interface SummaryView extends BaseView {}
}

@Data
public class BaseEntity
{
    @JsonView(BaseView.class)
    private Long id;    
}

public interface BaseView {}

A custom HandlerMethodReturnValueHandler is then wired into Spring MVC's context to detect the @ResponseView annotation, and apply the Jackson view accordingly.

I've supplied full code over on my blog.




回答2:


You need to manually wire in the MappingJacksonHttpMessageConverter. In spring 3.1 you are able to use the mvc xml tags like the following:

<mvc:annotation-driven >
    <mvc:message-converter>
        <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
    </mvc:message-converters>
</mvc:annotation-driven>

It is pretty ugly to not use spring 3.1, it will save you about 20 lines of xml. The mvc:annotation tag does ALOT.

You will need to wire in the object mapper with the correct view writer. I have noticed recently the using a @Configuration class can make complicated wiring like this a lot easier. Use a @Configuration class and create a @Bean with your MappingJacksonHttpMessageConverter, and wire the reference to that bean instead of the MappingJacksonHttpMessageConverter above.




回答3:


I've manage to solve the problem this way:

  • Create custom abstract class to contain the json response object:
public abstract AbstractJson<E>{
    @JsonView(Views.Public.class)
    private E responseObject;

    public E getResponseObject() {
        return responseObject;
    }
    public void setResponseObject(E responseObject) {
        this.responseObject = responseObject;
    }
}
  • Create a class for each visibility (just to mark the response):
public class PublicJson<E> extends AbstractJson<E> {}
public class ExtendedPublicJson<E> extends AbstractJson<E> {}
public class InternalJson<E> extends AbstractJson<E> {}
  • Change your method declaration:
    @RequestMapping(value = "/bean")
    @ResponseBody
    public final PublicJson<Bean> getBean() throws JsonGenerationException, JsonMappingException, IOException {
         return new PublicJson(new Bean());
    }
  • Create customs MessageConverter:
public class PublicJsonMessageConverter extends MappingJacksonHttpMessageConverter{

    public PublicApiResponseMessageConverter(){
        super();
        org.codehaus.jackson.map.ObjectMapper objMapper=new org.codehaus.jackson.map.ObjectMapper();
        objMapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
        objMapper.setSerializationConfig(objMapper.getSerializationConfig().withView(Views.Public.class));
        this.setObjectMapper(objMapper);
    }

     public boolean canWrite(Class<?> clazz, MediaType mediaType) {
         if(clazz.equals(PublicJson.class)){
             return true;
         }
         return false;
     }

}

public class ExtendedPublicJsonMessageConverter extends MappingJacksonHttpMessageConverter{

    public ExtendedPublicJsonMessageConverter(){
        super();
        org.codehaus.jackson.map.ObjectMapper objMapper=new org.codehaus.jackson.map.ObjectMapper();
        objMapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
        objMapper.setSerializationConfig(objMapper.getSerializationConfig().withView(Views.ExtendedPublic.class));
        this.setObjectMapper(objMapper);
    }

     public boolean canWrite(Class<?> clazz, MediaType mediaType) {
         if(clazz.equals(ExtendedPublicJson.class)){
             return true;
         }
         return false;
     }

}

public class InternalJsonMessageConverter extends MappingJacksonHttpMessageConverter{

    public InternalJsonMessageConverter(){
        super();
        org.codehaus.jackson.map.ObjectMapper objMapper=new org.codehaus.jackson.map.ObjectMapper();
        objMapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
        objMapper.setSerializationConfig(objMapper.getSerializationConfig().withView(Views.Internal.class));
        this.setObjectMapper(objMapper);
    }

     public boolean canWrite(Class<?> clazz, MediaType mediaType) {
         if(clazz.equals(Internal.class)){
             return true;
         }
         return false;
     }

}
  • Add the following to your xml:
<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="PublicJsonMessageConverter"></bean>
        <bean class="ExtendedPublicJsonMessageConverter"></bean>
        <bean class="InternalJsonMessageConverter"></bean>
    </mvc:message-converters>
</mvc:annotation-driven>

That's it! I had to update to spring 3.1 but that's all. I use the responseObject to send more info about the json call but you can override more methods of the MessageConverter to be completely transparent. I hope someday spring include an annotation for this.

Hope this helps!



来源:https://stackoverflow.com/questions/7793689/spring-and-jacksonjson-serialising-different-fields-with-views

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!