복붙노트

[SPRING] Spring MVC에서 http 요청 헤더를 기반으로 json을 동적으로 멋지게 출력하는 방법은 무엇입니까?

SPRING

Spring MVC에서 http 요청 헤더를 기반으로 json을 동적으로 멋지게 출력하는 방법은 무엇입니까?

Spring MVC Restcontroller에서 json 응답을 HTTP 매개 변수에 따라 동적으로 인쇄하고 싶습니다 (http://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api#pretty-print- gzip).

나는 꽤 정적 구성에 의해 인쇄를위한 구성을 찾았지만 어떻게 동적으로 그렇게하지?

Spring MVC를 REST 용으로 사용할 때 Jackson이 렌더링 된 JSON을 어떻게 멋지게 인쇄 할 수있게 할 수 있을까요?

그걸 어떻게하는 지 알기나 해?

해결법

  1. ==============================

    1.새로운 미디어 유형, 예를 들어 application / pretty + json을 정의하고 해당 미디어 유형으로 변환하는 새로운 HttpMessageConverter를 등록 할 수 있습니다. 사실, 클라이언트가 Accept : application / pretty + json 헤더로 요청을 보내면 우리의 새로운 HttpMessageConverter가 응답을 씁니다. 그렇지 않으면 일반 오래된 MappingJackson2HttpMessageConverter가이를 수행합니다.

    새로운 미디어 유형, 예를 들어 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를 등록하는 것이 더 나은 방법입니다.

  2. ==============================

    2.? pretty = true 매개 변수를 사용하여 예쁜 렌더링으로 전환하려면 사용자 지정 MappingJackson2HttpMessageConverter를 사용합니다.

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

    프랑크

  3. ==============================

    3.나는 Franck Lefebure의 접근법을 좋아하지만, 리플렉션을 사용하는 것을 좋아하지 않으므로, 여기에 사용자 정의 PrettyFormattedBody 유형 + 꽤 형식화 된 배열 / 목록을 사용하는 해결책이있다.

    나는 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.java (pretty는 선택적 요청 매개 변수 임) :

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

    4.Gson 포맷터가 사용 된 다른 솔루션 (전체 끌어 오기 요청 참조) :

    Gson 포맷터가 사용 된 다른 솔루션 (전체 끌어 오기 요청 참조) :

    Spring Config (2 beans 정의) :

    @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);
    }
    
  5. from https://stackoverflow.com/questions/36669172/how-to-enable-dynamic-pretty-print-of-json-based-on-http-request-header-in-sprin by cc-by-sa and MIT license