Джексон @JsonView, @JsonFilter и Spring
Можно ли использовать аннотации Jackson @JsonView
и @JsonFilter
для изменения JSON, возвращенного контроллером MVC Spring, при использовании аннотаций 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
, возможно, только его идентификатор. Затем в следующем вызове клиент может использовать этот идентификатор, чтобы запросить полный экземпляр ProductDescription
со всеми имеющимися свойствами.
Было бы идеально, если бы это можно было определить в методах контроллера Spring MVC, поскольку вызванный метод определяет контекст, в котором клиент запрашивал данные.
Ответы
Ответ 1
Эта проблема решена!
Следуйте this
Добавить поддержку представлений сериализации Jackson
Spring MVC теперь поддерживает представления сериализации Jackon для рендеринга разные подмножества одного POJO от другого контроллера методы (например, подробные страницы и сводный обзор). Проблема: SPR-7156
Это SPR-7156.
Статус: разрешено
Описание
Аннотации Jackson JSONView позволяют разработчику контролировать, какие аспекты метода являются сериализованными. В текущей реализации должен использоваться редактор просмотра Jackson, но тогда тип контента недоступен. Было бы лучше, если бы в аннотации RequestBody можно было указать JSONView.
Доступно на Spring ver >= 4.1
UPDATE
Следуйте за этой ссылкой . Объясняет пример аннотацией @JsonView.
Ответ 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>
Ответ 3
Я не знаю, как все работает с Spring (извините!), но Jackson 1.9 может использовать аннотацию @JsonView из методов JAX-RS, поэтому вы можете сделать:
@JsonView(ViewId.class)
@GET // and other JAX-RS annotations
public Pojo resourceMethod()
{
return new Pojo();
}
и Джексон будет использовать View, идентифицированный ViewId.class как активный вид. Возможно, Spring имеет (или будет иметь) аналогичную возможность? С JAX-RS это обрабатывается стандартным JacksonJaxrsProvider, для чего это стоит.
Ответ 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);
}
}
Ответ 5
Ответ на это после того, как много головокружительных тупиков и гадких ярости...
так просто. В этом случае у нас есть клиент bean со сложным адресом Address, встроенным в него, и мы хотим предотвратить сериализацию имени свойства surburb и street по адресу, когда выполняется сериализация json.
Мы делаем это, применяя аннотацию @JsonIgnoreProperties ({ "suburb" }) в полевом поле в классе Customer, количество игнорируемых полей Безграничный. например, я хочу проникнуть как в пригород, так и на улицу. Я бы аннотировал поле адреса с помощью @JsonIgnoreProperties ({ "пригород", "улица" })
Выполняя все это, мы можем создать архитектуру типа HATEOAS.
Ниже приведен полный код
Customer.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;
}
}
Address.java
public class Address {
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;
}
}
Ответ 6
В дополнение к @user356083 я внесла некоторые изменения, чтобы этот пример работал, когда возвращается @ResponseBody. Это немного взломать с помощью ThreadLocal, но 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);
}
}
Ответ 7
Еще лучше, начиная с версии 4.2.0. Вы можете сделать это просто следующим образом:
@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