如何在Spring MVC中基于http请求头启用json的动态漂亮打印?

问题描述 投票:5回答:4

我想基于http参数动态地从Spring MVC Restcontrollers打印json响应(如此处建议:http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print-gzip)。

我已经找到了通过静态配置进行漂亮打印的配置,但不是如何动态地进行打印?

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

知道怎么做吗?

java spring spring-mvc jackson
4个回答
5
投票

Introducing A New Media Type


您可以定义一个新的媒体类型,例如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表示。否则,MappingJackson2HttpMessageConverter会将一个紧凑的JSON写入响应主体。

你可以用ResponseBodyAdvice甚至拦截器实现同样的目标,但在我看来,注册一个全新的HttpMessageConverter是更好的方法。


1
投票

要使用?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);
        }
    }
}

弗兰克


1
投票

我喜欢Franck Lefebure's方法,但我不喜欢使用反射,所以这里是使用自定义PrettyFormattedBody类型+非常格式化的数组/列表的解决方案:

弹簧配置:

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

custom JSON response mapper.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(漂亮是一个可选的请求参数):

@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"
  ]
}

1
投票

使用Gson格式化程序的另一种解决方案(full pull request reference):

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

pretty G son message converter.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();
}
}

pretty formatted body.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);
}
© www.soinside.com 2019 - 2024. All rights reserved.