2016-04-16 8 views
5

Desidero stampare le risposte JSON di Spring MVC Restcontrollers in modo dinamico in base a un parametro http (come suggerito qui: http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print-gzip).Come abilitare la stampa dinamica di JSON in base all'intestazione della richiesta http in Spring MVC?

Ho trovato configurazioni per la stampa piuttosto con la configurazione statica, ma non come farlo dinamicamente?

When using Spring MVC for REST, how do you enable Jackson to pretty-print rendered JSON?

Qualsiasi idea di come farlo?

risposta

4

l'introduzione di un nuovo tipo di supporto


È possibile definire una nuova Tipo di supporto, diciamo, application/pretty+json e registrare un nuovo HttpMessageConverter che converte in quel tipo di supporto. Infatti, se il cliente invia una richiesta con l'intestazione Accept: application/pretty+json, il nostro nuovo HttpMessageConverter scriverà la risposta, altrimenti il ​​semplice vecchio MappingJackson2HttpMessageConverter lo farebbe.

Quindi, si estende il MappingJackson2HttpMessageConverter come segue:

public class PrettyPrintJsonConverter extends MappingJackson2HttpMessageConverter { 
    public PrettyPrintJsonConverter() { 
     setPrettyPrint(true); 
    } 

    @Override 
    public List<MediaType> getSupportedMediaTypes() { 
     return Collections.singletonList(new MediaType("application", "pretty+json")); 
    } 

    @Override 
    public boolean canWrite(Class<?> clazz, MediaType mediaType) { 
     boolean canWrite = super.canWrite(clazz, mediaType); 
     boolean canWritePrettily = mediaType != null && 
            mediaType.getSubtype().equals("pretty+json"); 

     return canWrite && canWritePrettily; 
    } 
} 

Che setPrettyPrint(true) nel costruttore farà il trucco per noi. Allora dovremmo registrare questo HttpMessageConverter:

@EnableWebMvc 
@Configuration 
public class WebConfig extends WebMvcConfigurerAdapter { 
    @Override 
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { 
     converters.add(new PrettyPrintJsonConverter()); 
    } 
} 

Come ho detto, se il client invia una richiesta con application/pretty+json Accetta intestazione, il nostro PrettyPrintJsonConverter scriverà la rappresentazione JSON Graziosamente. Altrimenti, MappingJackson2HttpMessageConverter scriverebbe un JSON compatto nel corpo della risposta.

È possibile ottenere lo stesso con un ResponseBodyAdvice o addirittura intercettori ma a mio parere, registrando un nuovo HttpMessageConverter è l'approccio migliore.

+0

Buon approccio. L'approccio alla testata è completo? "getHeader()" è uno dei diversi metodi per ottenere un valore di intestazione. Penso, devo sovrascrivere anche gli altri metodi. Destra? –

+1

Non ne sono sicuro, ma dovresti controllare un po 'del codice sorgente per scoprire quali metodi vengono usati per determinare il 'HttpMessageConverter'. Stavo solo cercando di darti l'idea di base .. –

1

Per passare alla bella prestazione con una bella = parametro vero io uso un MappingJackson2HttpMessageConverter personalizzato

@Configuration 
@RestController 
public class MyController { 

@Bean 
MappingJackson2HttpMessageConverter currentMappingJackson2HttpMessageConverter() { 
     MappingJackson2HttpMessageConverter jsonConverter = new CustomMappingJackson2HttpMessageConverter(); 
     return jsonConverter; 
} 


public static class Input { 
    public String pretty; 
} 

public static class Output { 
    @JsonIgnore 
    public String pretty; 
} 

@RequestMapping(path = "/api/test", method = {RequestMethod.GET, RequestMethod.POST}) 
Output test(@RequestBody(required = false) Input input, 
      @RequestParam(required = false, value = "pretty") String pretty) 
{ 
    if (input.pretty==null) input.pretty = pretty; 
    Output output = new Output(); 
    output.pretty = input.pretty; 
    return output; 
} 
} 

Il convertitore:?

public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { 

    ObjectMapper objectMapper; 

    ObjectMapper prettyPrintObjectMapper; 

    public CustomMappingJackson2HttpMessageConverter() { 
     objectMapper = new ObjectMapper(); 
     prettyPrintObjectMapper = new ObjectMapper(); 
     prettyPrintObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); 

    } 


    @Override 
    @SuppressWarnings("deprecation") 
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) 
      throws IOException, HttpMessageNotWritableException { 

     JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); 
     JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); 
     try { 
      writePrefix(generator, object); 

      Class<?> serializationView = null; 
      FilterProvider filters = null; 
      Object value = object; 
      JavaType javaType = null; 
      if (object instanceof MappingJacksonValue) { 
       MappingJacksonValue container = (MappingJacksonValue) object; 
       value = container.getValue(); 
       serializationView = container.getSerializationView(); 
       filters = container.getFilters(); 
      } 
      javaType = getJavaType(type, null); 

      ObjectMapper currentMapper = objectMapper; 
      Field prettyField = ReflectionUtils.findField(object.getClass(), "pretty"); 
      if (prettyField != null) { 
       Object prettyObject = ReflectionUtils.getField(prettyField, object); 
       if (prettyObject != null && prettyObject instanceof String) { 
        String pretty = (String)prettyObject; 
        if (pretty.equals("true")) 
         currentMapper = prettyPrintObjectMapper; 
       } 
      } 

      ObjectWriter objectWriter; 
      if (serializationView != null) { 
       objectWriter = currentMapper.writerWithView(serializationView); 
      } 
      else if (filters != null) { 
       objectWriter = currentMapper.writer(filters); 
      } 
      else { 
       objectWriter = currentMapper.writer(); 
      } 
      if (javaType != null && javaType.isContainerType()) { 
       objectWriter = objectWriter.withType(javaType); 
      } 
      objectWriter.writeValue(generator, value); 

      writeSuffix(generator, object); 
      generator.flush(); 

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

Franck

0

Mi piace Franck Lefebure's approccio, ma io non mi piace il riflesso usato, quindi ecco una soluzione con l'uso di tipo PrettyFormattedBody personalizzato + array/liste piuttosto formattati:

Primavera Config:

@Bean 
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { 
    return new CustomJsonResponseMapper(); 
} 

CustomJsonResponseMapper.java:

public class CustomJsonResponseMapper extends MappingJackson2HttpMessageConverter { 

    private final ObjectMapper prettyPrintObjectMapper; 

    public CustomJsonResponseMapper() { 
     super(); 
     prettyPrintObjectMapper = initiatePrettyObjectMapper(); 
    } 

    protected ObjectMapper initiatePrettyObjectMapper() { 
     // clone and re-configure default object mapper 
     final ObjectMapper prettyObjectMapper = objectMapper != null ? objectMapper.copy() : new ObjectMapper(); 
     prettyObjectMapper.configure(SerializationFeature.INDENT_OUTPUT, true); 

     // for arrays - use new line for every entry 
     DefaultPrettyPrinter pp = new DefaultPrettyPrinter(); 
     pp.indentArraysWith(new DefaultIndenter()); 
     prettyObjectMapper.setDefaultPrettyPrinter(pp); 

     return prettyObjectMapper; 
    } 

    @Override 
    protected void writeInternal(final Object objectToWrite, final Type type, final HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { 

     // based on: if objectToWrite is PrettyFormattedBody with isPretty == true => use custom formatter 
     // otherwise - use the default one 

     final Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite) 
       .filter(o -> o instanceof PrettyFormattedBody) 
       .map(o -> (PrettyFormattedBody) objectToWrite); 

     final boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false); 
     final Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite); 

     if (pretty) { 
      // this is basically full copy of super.writeInternal(), but with custom (pretty) object mapper 
      MediaType contentType = outputMessage.getHeaders().getContentType(); 
      JsonEncoding encoding = getJsonEncoding(contentType); 
      JsonGenerator generator = this.prettyPrintObjectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); 
      try { 
       writePrefix(generator, realObject); 

       Class<?> serializationView = null; 
       FilterProvider filters = null; 
       Object value = realObject; 
       JavaType javaType = null; 
       if (realObject instanceof MappingJacksonValue) { 
        MappingJacksonValue container = (MappingJacksonValue) realObject; 
        value = container.getValue(); 
        serializationView = container.getSerializationView(); 
        filters = container.getFilters(); 
       } 
       if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { 
        javaType = getJavaType(type, null); 
       } 
       ObjectWriter objectWriter; 
       if (serializationView != null) { 
        objectWriter = this.prettyPrintObjectMapper.writerWithView(serializationView); 
       } else if (filters != null) { 
        objectWriter = this.prettyPrintObjectMapper.writer(filters); 
       } else { 
        objectWriter = this.prettyPrintObjectMapper.writer(); 
       } 
       if (javaType != null && javaType.isContainerType()) { 
        objectWriter = objectWriter.forType(javaType); 
       } 

       objectWriter.writeValue(generator, value); 

       writeSuffix(generator, realObject); 
       generator.flush(); 

      } catch (JsonProcessingException ex) { 
       throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); 
      } 
     } else { 
      // use default formatting if isPretty property is not specified 
      super.writeInternal(realObject, type, outputMessage); 
     } 
    } 

    @Override 
    public boolean canWrite(Class<?> clazz, MediaType mediaType) { 
     // this should be mandatory overridden, 
     // otherwise writeInternal() won't be called with custom PrettyFormattedBody type 
     return (PrettyFormattedBody.class.equals(clazz) && canWrite(mediaType)) || super.canWrite(clazz, mediaType); 
    } 

    public static final class PrettyFormattedBody { 
     private final Object body; 
     private final boolean pretty; 

     public PrettyFormattedBody(Object body, boolean pretty) { 
      this.body = body; 
      this.pretty = pretty; 
     } 

     public Object getBody() { 
      return body; 
     } 

     public boolean isPretty() { 
      return pretty; 
     } 
    } 
} 

HealthController.java (abbastanza è un parametro opzionale richiesta):

@RequestMapping(value = {"/", "/health"}, 
     produces = APPLICATION_JSON_VALUE) 
public ResponseEntity<?> health(@RequestParam Optional<String> pretty) { 
    return new ResponseEntity<>(
      new CustomJsonResponseMapper.PrettyFormattedBody(healthResult(), pretty.isPresent()), 
      HttpStatus.OK); 
} 

esempio di risposta http://localhost:8080:

{"status":"OK","statusCode":200,"endpoints":["/aaa","/bbb","/ccc"]} 

esempio di risposta http://localhost:8080?pretty:

{ 
    "status": "OK", 
    "statusCode": 200, 
    "endpoints": [ 
    "/aaa", 
    "/bbb", 
    "/ccc" 
    ] 
} 
0

Un'altra soluzione se si utilizza Gson formattatore (full pull request reference):

Primavera Config (definire 2 fagioli):

@Bean 
public Gson gson() { 
    return new GsonBuilder() 
      .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) 
      .disableHtmlEscaping() 
      .create(); 
} 

/** 
* @return same as {@link #gson()}, but with <code>{@link Gson#prettyPrinting} == true</code>, e.g. use indentation 
*/ 
@Bean 
public Gson prettyGson() { 
    return new GsonBuilder() 
      .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY) 
      .setPrettyPrinting() 
      .disableHtmlEscaping() 
      .create(); 
} 

/** 
* Custom JSON objects mapper: uses {@link #gson()} as a default JSON HTTP request/response mapper 
* and {@link #prettyGson()} as mapper for pretty-printed JSON objects. See {@link PrettyGsonMessageConverter} for 
* how pretty print is requested. 
* <p> 
* <b>Note:</b> {@link FieldNamingPolicy#IDENTITY} field mapping policy is important at least for 
* {@link PaymentHandleResponse#getPayment()} method. See respective documentation for details. 
* 
* @return default HTTP request/response mapper, based on {@link #gson()} bean. 
*/ 
@Bean 
public GsonHttpMessageConverter gsonMessageConverter() { 
    return new PrettyGsonMessageConverter(gson(), prettyGson()); 
} 

PrettyGsonMessageConverter.java:

/** 
* Custom Gson response message converter to allow JSON pretty print, if requested. 
* <p> 
* The class extends default Spring {@link GsonHttpMessageConverter} adding {@link #prettyGson} mapper and processing 
* {@link PrettyFormattedBody} instances. 
*/ 
public class PrettyGsonMessageConverter extends GsonHttpMessageConverter { 

/** 
* JSON message converter with configured pretty print options, which is used when a response is expected to be 
* pretty printed. 
*/ 
private final Gson prettyGson; 

/** 
* @see GsonHttpMessageConverter#jsonPrefix 
*/ 
private String jsonPrefix; 

/** 
* @param gson  default (minified) JSON mapper. This value is set to {@code super.gson} property. 
* @param prettyGson pretty configure JSON mapper, which is used if the body expected to be pretty printed 
*/ 
public PrettyGsonMessageConverter(final Gson gson, final Gson prettyGson) { 
    super(); 
    this.setGson(gson); 
    this.prettyGson = prettyGson; 
} 

/** 
* Because base {@link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded 
* {@link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value. 
* 
* @see GsonHttpMessageConverter#setJsonPrefix(String) 
*/ 
@Override 
public void setJsonPrefix(String jsonPrefix) { 
    super.setJsonPrefix(jsonPrefix); 
    this.jsonPrefix = jsonPrefix; 
} 

/** 
* Because base {@link GsonHttpMessageConverter#jsonPrefix} is private, but is used in overloaded 
* {@link #writeInternal(Object, Type, HttpOutputMessage)} - we should copy this value. 
* 
* @see GsonHttpMessageConverter#setPrefixJson(boolean) 
*/ 
@Override 
public void setPrefixJson(boolean prefixJson) { 
    super.setPrefixJson(prefixJson); 
    this.jsonPrefix = (prefixJson ? ")]}', " : null); 
} 

/** 
* Allow response JSON pretty print if {@code objectToWrite} is a {@link PrettyFormattedBody} instance with 
* <code>{@link PrettyFormattedBody#isPretty() isPretty} == true</code>. 
* 
* @param objectToWrite if the value is {@link PrettyFormattedBody} instance with 
*      <code>{@link PrettyFormattedBody#isPretty() isPretty} == true</code> - use 
*      {@link #prettyGson} for output writing. Otherwise use base 
*      {@link GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage)} 
* @param type   the type of object to write (may be {@code null}) 
* @param outputMessage the HTTP output message to write to 
* @throws IOException      in case of I/O errors 
* @throws HttpMessageNotWritableException in case of conversion errors 
*/ 
@Override 
protected void writeInternal(@Nullable final Object objectToWrite, 
          @Nullable final Type type, 
          @Nonnull final HttpOutputMessage outputMessage) 
     throws IOException, HttpMessageNotWritableException { 

    // based on: if objectToWrite is PrettyFormattedBody && isPretty == true => use custom formatter 
    // otherwise - use the default base GsonHttpMessageConverter#writeInternal(Object, Type, HttpOutputMessage) 

    Optional<PrettyFormattedBody> prettyFormatted = Optional.ofNullable(objectToWrite) 
      .filter(o -> o instanceof PrettyFormattedBody) 
      .map(o -> (PrettyFormattedBody) objectToWrite); 

    boolean pretty = prettyFormatted.map(PrettyFormattedBody::isPretty).orElse(false); 
    Object realObject = prettyFormatted.map(PrettyFormattedBody::getBody).orElse(objectToWrite); 

    if (pretty) { 
     // this is basically full copy of super.writeInternal(), but with custom (pretty) gson mapper 
     Charset charset = getCharset(outputMessage.getHeaders()); 
     OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset); 
     try { 
      if (this.jsonPrefix != null) { 
       writer.append(this.jsonPrefix); 
      } 
      if (type != null) { 
       this.prettyGson.toJson(realObject, type, writer); 
      } else { 
       this.prettyGson.toJson(realObject, writer); 
      } 
      writer.close(); 
     } catch (JsonIOException ex) { 
      throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); 
     } 
    } else { 
     // use default writer if isPretty property is not specified 
     super.writeInternal(realObject, type, outputMessage); 
    } 
} 

/** 
* To ensure the message converter supports {@link PrettyFormattedBody} instances 
* 
* @param clazz response body class 
* @return <b>true</b> if the {@code clazz} is {@link PrettyFormattedBody} or {@code super.supports(clazz) == true} 
*/ 
@Override 
protected boolean supports(Class<?> clazz) { 
    return PrettyFormattedBody.class.equals(clazz) || super.supports(clazz); 
} 

/** 
* Just a copy-paste of {@link GsonHttpMessageConverter#getCharset(HttpHeaders)} because it is private, but used in 
* {@link #writeInternal(Object, Type, HttpOutputMessage)} 
* 
* @param headers output message HTTP headers 
* @return a charset from the {@code headers} content type or {@link GsonHttpMessageConverter#DEFAULT_CHARSET} 
* otherwise. 
*/ 
private Charset getCharset(HttpHeaders headers) { 
    if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) { 
     return DEFAULT_CHARSET; 
    } 
    return headers.getContentType().getCharset(); 
} 
} 

PrettyFormattedBody.java:

public final class PrettyFormattedBody { 
private final Object body; 
private final boolean pretty; 

private PrettyFormattedBody(@Nonnull final Object body, final boolean pretty) { 
    this.body = body; 
    this.pretty = pretty; 
} 

public Object getBody() { 
    return body; 
} 

public boolean isPretty() { 
    return pretty; 
} 

public static PrettyFormattedBody of(@Nonnull final Object body, final boolean pretty) { 
    return new PrettyFormattedBody(body, pretty); 
} 
} 

e infine - il controller stesso:

@RequestMapping(
     value = {"/health", "/"}, 
     produces = APPLICATION_JSON_VALUE) 
public ResponseEntity<?> checkHealth(@RequestParam(required = false) String pretty, 
            @Autowired ApplicationInfo applicationInfo) { 
    Map<String, Object> tenantResponse = new HashMap<>(); 
    tenantResponse.put(APP_INFO_KEY, applicationInfo); 

    return new ResponseEntity<>(PrettyFormattedBody.of(tenantResponse, pretty != null), 
      HttpStatus.OK); 
}