簡體   English   中英

如何在Spring MVC中基於http請求標頭啟用json的動態漂亮打印?

[英]How to enable dynamic pretty print of json based on http request header in Spring MVC?

我想基於http參數動態地打印來自Spring MVC Restcontrollers的json響應(如此處建議: http : //www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print- gzip )。

我已經找到了可以通過靜態配置漂亮地打印出來的配置,但是沒有動態地做到這一點?

在將Spring MVC用於REST時,如何使Jackson能夠漂亮地打印呈現的JSON?

任何想法如何做到這一點?

引入新的媒體類型


您可以定義一個新的媒體類型 ,例如application/pretty+json並注冊一個新的HttpMessageConverter ,它將轉換為該媒體類型。 實際上,如果客戶端發送帶有Accept: application/pretty+json標頭的請求,則我們的新HttpMessageConverter將編寫響應,否則,普通的舊MappingJackson2HttpMessageConverter會執行該操作。

因此,如下擴展MappingJackson2HttpMessageConverter

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;
    }
}

構造函數中的setPrettyPrint(true)將為我們解決問題。 然后我們應該注冊這個HttpMessageConverter

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

如我所說,如果客戶端發送帶有application/pretty+json Accept頭的請求,我們的PrettyPrintJsonConverter將編寫JSON表示形式Prettily 否則, MappingJackson2HttpMessageConverter會將緊湊的JSON寫入響應主體。

您可以使用ResponseBodyAdvice甚至是Interceptors來實現相同的目的,但我認為,注冊全新的HttpMessageConverter是更好的方法。

要使用?pretty = true參數切換到漂亮渲染,我使用自定義MappingJackson2HttpMessageConverter

@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;
}
}

轉換器:

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 Lefebure的方法,但是我不喜歡使用反射,因此這是使用自定義PrettyFormattedBody類型+漂亮格式的數組/列表的解決方案:

彈簧配置

@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.javapretty是可選的請求參數):

@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);
}

響應示例http://localhost:8080

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

響應示例http://localhost:8080?pretty

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

如果使用Gson格式化程序的另一種解決方案( 完整請求參考 ):

Spring Config (定義2個bean):

@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);
}
}

最后-控制器本身:

 @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);
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM