转载

springboot入门05 – 包装SpringBoot Controller返回值

一个项目使用了SpringBoot,需要对Controller的返回值进行二次包装。包装类结构大致如下:

import org.springframework.http.HttpStatus;
 
public class Result {
 
    private int status = HttpStatus.OK.value();
 
    private Object content;
 
    public Result(Object content) {
        this.content = content;
    }
 
    public Result(int status, String content) {
        this(content);
        this.content = content;
    }
 
 
    public int getStatus() {
        return status;
    }
 
    public void setStatus(int status) {
        this.status = status;
    }
 
    public Object getContent() {
        return content;
    }
 
    public void setContent(Object content) {
        this.content = content;
    }
}

通过查找资料,找到了两种封装方式。

方法一

第一种方式是替换掉RequestResponseBodyMethodProcessor,这需要使用一个MethodReturnValueHandler的装饰类:

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
 
 
public class ResponseBodyWrapHandler implements HandlerMethodReturnValueHandler {
 
    private final HandlerMethodReturnValueHandler delegate;
 
    public ResponseBodyWrapHandler(HandlerMethodReturnValueHandler delegate) {
        this.delegate = delegate;
    }
 
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return delegate.supportsReturnType(returnType);
    }
 
    @Override
    public void handleReturnValue(Object returnValue,
                                  MethodParameter returnType,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest) throws Exception {
        Result result = new Result(returnValue);
        delegate.handleReturnValue(result, returnType, mavContainer, webRequest);
    }
}

在装饰类中使用一个Result类的实例替换了returnValue。而后在InitializingBean中基于原来的RequestResponseBodyMethodProcessor的实例创建一个ResponseBodyWrapHandler的实例来完成替换:

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
 
import java.util.ArrayList;
import java.util.List;
 
public class ResponseBodyWrapFactoryBean implements InitializingBean {
 
    @Autowired
    private RequestMappingHandlerAdapter adapter;
 
    @Override
    public void afterPropertiesSet() throws Exception {
        List<HandlerMethodReturnValueHandler> returnValueHandlers = adapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList(returnValueHandlers);
        decorateHandlers(handlers);
        adapter.setReturnValueHandlers(handlers);
    }
 
 
    private void decorateHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        for (HandlerMethodReturnValueHandler handler : handlers) {
            if (handler instanceof RequestResponseBodyMethodProcessor) {
                ResponseBodyWrapHandler decorator = new ResponseBodyWrapHandler(handler);
                int index = handlers.indexOf(handler);
                handlers.set(index, decorator);
                break;
            }
        }
    }
 
}

使用ResponseBodyWrapFactoryBean,完成afterProperties方法的调用,只需要创建一个ResponseBodyWrapFactoryBean的实例即可:

    @Bean
    public ResponseBodyWrapFactoryBean getResponseBodyWrap() {
        return new ResponseBodyWrapFactoryBean();
    }

这行代码可以放在启动类中。

方法二

第二种方式基于ControllerAdvice和HttpMessageConverter实现。

首先用一个ResponseBodyAdvice类的实现包装Controller的返回值:

import com.zhyea.spring.ext.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
 
 
@ControllerAdvice
public class ResponseAdvisor 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) {
 
        String requestPath = request.getURI().getPath();
        if (!requestPath.startsWith("/ug")) {
            return body;
        }
 
        if (body instanceof Result) {
            return body;
        }
 
        return new Result(body);
    }
}

如果Controller类的返回值没有String类型的,仅有上面这个类就够了。如果有String类型的返回值,就有可能遇到类型不匹配的问题。HttpMessageConverter是根据Controller的原始返回值类型进行处理的,而我们在ResponseAdvisor中改变了返回值的类型。如果HttpMessageConverter处理的目标类型是Object还好说,如果是其它类型就会出现问题,其中最容易出现问题的就是String类型,因为在所有的HttpMessageConverter实例集合中,StringHttpMessageConverter要比其它的Converter排得靠前一些。我们需要尝试将处理Object类型的HttpMessageConverter放得靠前一些,这可以在一个Configuration类中完成:

@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
 
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter());
        super.configureMessageConverters(converters);
    }
 
}

在这个方案中,如需要对异常做些特别处理,还可以创建一个ExceptionAdvisor类来完成:

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
 
 
@ControllerAdvice
public class ExceptionAdvisor {
 
    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    @ResponseStatus
    public Result exceptionHandler(Exception e) {
        return new Result(HttpStatus.BAD_REQUEST.value(), e.getMessage());
    }
 
    @ResponseBody
    @ExceptionHandler(value = RuntimeException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public Result formatCheckExceptionHandler(RuntimeException e) {
        return new Result(HttpStatus.BAD_REQUEST.value(), e.getMessage());
    }
 
}

这样还可以根据异常类型来设置返回时的HttpStatus。

就这样。

有朋友在评论区指出问题了,做了些调整,也写了一份示例程序上传到CSDN。有兴趣的可以下载来看看。下载地址如下: 点击此处下载 。

这是许久之前刚用springboot时写的,现在适应springboot的最新版本存在一些问题。所以稍稍重新调整了下,并将之加入到了最近正在尝试进行的一个系列里面。

新版本的代码可以在 GitHub / zhyea 上看到。

##########

原文  https://www.zhyea.com/2019/12/01/wrap-spring-boot-controller-response.html
正文到此结束
Loading...