0

I use springboot framework. I need to encrypt body data(from json to encrypted string). So I used ResponseBodyAdvice.java with @ControllerAdvice but there was a problem that it always responds the encrypted data with double quotes(e.g. "hello") I need to respond just hello instead of "hello".

@Override
public Object beforeBodyWrite(...) {
response.getHeaders().set("content-type", "text/plain;charset=UTF-8");

//some codes..

String result = "hello";

return result;
}

It responds "hello" (I need to the data without double quotes)

In controller class, it responds just hello(without double quotes). See the below code.


  @ApiOperation(value = "absdfasdf", produces = "text/plain")
  @GetMapping("/absd")
  public String asdfasdf() {
    return "hello";
  }

P.J.Meisch
  • 18,013
  • 6
  • 50
  • 66
RedWhale
  • 7
  • 1
  • 5
  • If it's a json response that's how strings are represented. Are you saying you want to reply to a json request with something other than json? – Dave Newton Oct 11 '19 at 05:58
  • you can visit this question [https://stackoverflow.com/questions/1205135/how-to-encrypt-string-in-java](https://stackoverflow.com/questions/1205135/how-to-encrypt-string-in-java) its maybe like your problem. – Mehmet Onar Oct 11 '19 at 06:07
  • @MehmetOnar Thanks your comment but it's not my problem. The problem is not related to encryption. – RedWhale Oct 11 '19 at 06:31
  • @DaveNew This link https://stackoverflow.com/questions/33688208/webapi2-return-simple-string-without-quotation-mark is exactly what I want in ResponseBodyAdvice. How can I control it? – RedWhale Oct 11 '19 at 06:38

1 Answers1

3

You need to check your MessageConverters to make sure StringMessageConverter is before MappingJackson2HttpMessageConverter. Otherwise, JSON MessageConverter will be selected to serialize string and add the extra double quotes.

@Component
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // check converter order here
    }
}

As the following spring source code, the flow is:

  1. choose a message converter
  2. invoke beforeBodyWrite method in ResponseBodyAdvice
  3. convert message

AbstractMessageConverterMethodProcessor.java

// choose a message converter
if (genericConverter != null ?
                        ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
                        converter.canWrite(valueType, selectedMediaType)) {
                    // invoke beforeBodyWrite
                    body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
                            (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                            inputMessage, outputMessage);
                    if (body != null) {
                        Object theBody = body;
                        LogFormatUtils.traceDebug(logger, traceOn ->
                                "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
                        addContentDispositionHeader(inputMessage, outputMessage);
                        if (genericConverter != null) {
                            genericConverter.write(body, targetType, selectedMediaType, outputMessage);
                        }
                        else {
                            // convert message
                            ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
                        }
                    }

The truth is that we can not change MessageConverter in ResponseBodyAdvice. But we can custom a Dynamic MessageConverter. For example:

public class DynamicMessageConverter implements HttpMessageConverter<Object> {

    private final HttpMessageConverter<Object> jsonConverter;
    private final HttpMessageConverter<String> stringConverter;

    public DynamicMessageConverter(HttpMessageConverter<Object> jsonConverter, HttpMessageConverter<String> stringConverter) {
        this.jsonConverter = jsonConverter;
        this.stringConverter = stringConverter;
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        return jsonConverter.canWrite(clazz, mediaType) || stringConverter.canWrite(clazz, mediaType);
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        List<MediaType> jsonMediaTypes = jsonConverter.getSupportedMediaTypes();
        List<MediaType> stringMediaTypes = stringConverter.getSupportedMediaTypes();
        List<MediaType> all = new ArrayList<>();
        all.addAll(jsonMediaTypes);
        all.addAll(stringMediaTypes);
        return all;
    }

    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        if (o instanceof String) {
            stringConverter.write((String) o, contentType, outputMessage);
        } else {
            jsonConverter.write(o, contentType, outputMessage);
        }
    }

    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public Object read(Class clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
        throw new UnsupportedOperationException();
    }
}

And then enable it

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        DynamicMessageConverter dynamicMessageConverter = new DynamicMessageConverter(jsonConverter, stringConverter);
        converters.add(0, dynamicMessageConverter);
    }

Writing directly through response seems more concise.

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        response.getHeaders().set("content-type", "text/plain;charset=UTF-8");

        //some codes..

        String result = "hello";

        try (OutputStream stream = response.getBody()) {
            stream.write(result.getBytes("utf-8"));
            stream.flush();
        } catch (IOException e) {
            // log ex
        }
        return null;
    }
sunshujie
  • 91
  • 4
  • I already added the converters like this converters.add(0, new StringHttpMessageConverter()); and converters.add(1, new MappingJackson2HttpMessageConverter());. – RedWhale Oct 11 '19 at 07:42
  • But it's not a solution because MappingJackson2HttpMessageConverter is already selected because I control the body data in ResponseBodyAdvice.java.. The flow is.. controller -> json body(with MappingJackson2HttpMessageConverter) -> encrypt the body data(using ResponseBodyAdvice) -> output – RedWhale Oct 11 '19 at 07:47
  • Wow!!! Yes!! That is exactly what I wanted! Thank you :) I couldn't think the outputstream directly get and write in ResponseBodyAdvice! – RedWhale Oct 11 '19 at 09:27
  • How did you find AbstractMessageConverterMethodProcessor.java about the beforeBodyWrite() process? I tested again, again, again about the process..and I realized the process. but you checked the source. I'm wondering about the way. – RedWhale Oct 11 '19 at 10:20
  • @RedWhale I find AbstractMessageConverterMethodProcessor.java by the debug stack trace. – sunshujie Oct 11 '19 at 12:08