简体   繁体   中英

Spring + Jackson: Wrapping Response Body in response object

This may be a strange question, although I wonder why it hasn't been asked or proposed before... so please correct me if any ignorance.

First off, I am using Jackson in conjunction with Spring and the @ResponseBody annotation. Currently, for every request handler I am returning a "Response" wrapper object, as that is what the client expects. This wrapper is quite simple:

{ "response": { "data" : ACTUAL_DATA } }

Thing is, I'm not a fan of explicitly wrapping each return value for all my request handlers. I also do not like having to unwrap these response wrappers in my unit tests.

Rather, I wonder if it were possible to return the ACTUAL_DATA as it were, and to intercept and wrap this data elsewhere.

If this is in fact possible, would it then be possible to read the annotations attached to the intercepted request handler? This way I can use custom annotations to decide how to wrap the data.

For example something like this would be amazing (note that @FetchResponse and @ResponseWrapper are made up proposed annotations):

@RequestMapping(...)
@FetchResponse
@ResponseBody
public List<User> getUsers() {
    ...
}

@ResponseWrapper(FetchResponse.class)
public Object wrap(Object value) {
    ResponseWrapper rw = new ResponseWrapper();
    rw.setData(value);
    return rw;
}

Anyone familiar with this territory? Or alternatively, and reasons why this might be bad practice?

好吧,看起来我正在寻找 Spring 的“ResponseBodyAdvice”和“AbstractMappingJacksonResponseBodyAdvice”。

For anybody looking on more info on this topic: I was facing the same issue as well, and thanks to the tip of kennyg, i managed to come up with the following solution:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class JSendAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof JSendResponse) {
            return body;
        }

        return new JSendResponse<>().success(body);
    }
}

This solution wraps all the objects returned in your controllers inside a (for this example) JSendResponse class, which saves you the hassle of returning JSendResponses in all of your controller methods.

I know it's been a while since the answer was accepted but I recently stumbled onto an issue with Jackson that allowed me to discover a problem with using ResponseBodyAdvice .

Jackson will not correctly serialize your polymorphic types that use @JsonTypeInfo / @JsonSubTypes if during runtime the values of your types are not known: ie for example if you have a generic container type like class ResponseWrapper<T> { List<T> objects; } class ResponseWrapper<T> { List<T> objects; } . That is unless you provide Jackson with specialization of that generic type before you ask it to serialize your value, refer to Why does Jackson polymorphic serialization not work in lists? . Spring does this for you when you return say a list of T and that T is known because it's provided explicitly in the method return type (as in public List<MyEntity> getAllEntities(); ).

If you simply implement ResponseBodyAdvice and return a new, wrapped value from beforeBodyWrite() then Spring will no longer know your full generic type with its specialization, and it will serialize your response as ResponseWrapper<?> instead of ResponseWrapper<MyEntity> .

The only way around this is to both extend from AbstractJackson2HttpMessageConverter and override writeInternal() . See how the method treats the type here: https://github.com/spring-projects/spring-framework/blob/master/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java#L437
And you also need to implement a Controller advice using AbstractMappingJacksonResponseBodyAdvice and your own custom MappingJacksonValue that includes Type targetType that custom HttpMessageConverter will use.

ResponseWrapper

public class ResponseWrapper<T> {
    @Nullable Error error;
    T result;

    public ResponseWrapper(T result) {
        this.result = result;
    }
}

WrappingAdvice

@Component
public class WrappingAdvice extends AbstractMappingJacksonResponseBodyAdvice {
    

    @Override
    protected MappingJacksonValue getOrCreateContainer(Object body) {
        MappingJacksonValue cnt = super.getOrCreateContainer(body);
        if (cnt instanceof MyMappingJacksonValue) {
            return cnt;
        }

        return new MyMappingJacksonValue(cnt);
    }

    @Override
    protected void beforeBodyWriteInternal(
            MappingJacksonValue bodyContainer, MediaType contentType,
            MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {

        MyMappingJacksonValue cnt = (MyMappingJacksonValue) bodyContainer;

        Type targetType = getTargetType(bodyContainer.getValue(), returnType);

        cnt.setValue(new ResponseWrapper(cnt.getValue()));
        cnt.setTargetType(TypeUtils.parameterize(
                ResponseWrapper.class,
                targetType));
    }

    /**
     * This is derived from AbstractMessageConverterMethodProcessor
     */
    private Type getTargetType(Object value, MethodParameter returnType) {
        if (value instanceof CharSequence) {
            return String.class;
        }

        Type genericType;
        if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
            genericType = ResolvableType.forType(returnType.getGenericParameterType()).getGeneric().getType();
        } else {
            genericType = returnType.getGenericParameterType();
        }

        return GenericTypeResolver.resolveType(genericType, returnType.getContainingClass());
    }

    public static class MyMappingJacksonValue extends MappingJacksonValue {
        private Type targetType;

        public MyMappingJacksonValue(MappingJacksonValue other) {
            super(other.getValue());
            setFilters(other.getFilters());
            setSerializationView(other.getSerializationView());
        }

        public Type getTargetType() {
            return targetType;
        }

        public void setTargetType(Type targetType) {
            this.targetType = targetType;
        }
    }
}

JsonHttpMessageBodyConverter

@Component
public class JsonHttpMessageBodyConverter extends AbstractJackson2HttpMessageConverter {

    // omitted all constructors

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        if (object instanceof WrapAPIResponseAdvice.MyMappingJacksonValue) {
            type = ((WrapAPIResponseAdvice.MyMappingJacksonValue) object).getTargetType();
        }

        super.writeInternal(object, type, outputMessage);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM