我想基于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?
知道怎么做吗?
答案 0 :(得分:5)
您可以定义新的媒体类型,例如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
甚至拦截器实现相同目标,但在我看来,注册一个全新的HttpMessageConverter
是更好的方法。
答案 1 :(得分: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);
}
}
}
弗兰克
答案 2 :(得分:1)
我喜欢Franck Lefebure's方法,但我不喜欢使用反射,所以这里有一个使用自定义PrettyFormattedBody
类型+非常格式化的数组/列表的解决方案:
Spring 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 (漂亮是可选的请求参数):
@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"
]
}
答案 3 :(得分: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());
}
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);
}