8 Spring Boot返回資料及異常統一封裝

icarusliu81發表於2018-05-30

專案開發中,一般情況下對資料返回的格式可能會有一個統一的要求,一般會包括狀態碼、資訊及資料三部分。舉個例子,假設規範要求資料返回的結構如下所示:

{"data":[{"id":5,"userId":5,"name":"test1","articleCount":0}],"errorMessage":"","statusCode":"200"}

其中,data欄位儲存實際的返回資料;errorMessage儲存當出現異常時的異常資訊;statusCode儲存處理碼;一般用一個特殊的碼如200來表示無異常;而出現異常時可以儲存具體的異常碼。

要返回這樣的資料,最直接的做法當然是在每一個Controller中去處理,返回的資料本身就封裝有處理碼、資料、出現異常時的異常資訊等欄位。這樣做導致的問題,就是每一個Controller向外暴露的方法都要建立一個返回的物件來封裝這種處理,並在出現異常時捕獲異常進行處理。

因此最好是能夠統一處理這種轉換,這樣的話服務提供者就只需關注他原本就需要處理的事情:一是在無異常時返回資料本身;二是在出現異常時丟擲合適的異常。

為達到統一處理的目的,需要針對兩個場景做單獨的處理:一是當無異常時,在原返回的資料基礎上封裝一層,將狀態碼等資訊包含進來;二是當出現異常時,將異常資訊進行封裝然後返回給呼叫方。

1. 執行無異常時返回資料封裝

在Spring Boot中,針對返回值的處理是在HandlerAdapter的returnValueHandlers中進行的。我們先嚐試建立一個ReturnValueHandler物件實現HandlerMethodReturnValueHandler介面,然後通過WebMvcConfigurer中的addReturnValueHandlers將其新增。如下所示:

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Autowired
    private RestReturnValueHandler restReturnValueHandler;

    @Override
    public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
        handlers.clear();
        handlers.add(restReturnValueHandler);
    }
}

其中RestReturnValueHanlder定義如下:

/**
 * REST型別的返回值處理器
 * 將REST的返回結果進行進一步的封裝,如原本返回的是data,那麼封裝後將會是:
 * {statusCode: '', errorMessage: '', exception: {}, data: data}
 *
 * @author LiuQI 2018/5/30 10:48
 * @version V1.0
 **/
@Component
public class RestReturnValueHandler implements HandlerMethodReturnValueHandler {
    @Autowired
    private MessageConverter messageConverter;

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        if (returnType.hasMethodAnnotation(ResponseBody.class)
                || (!returnType.getDeclaringClass().equals(ModelAndView.class))
                    && returnType.getMethod().getDeclaringClass().isAnnotationPresent(RestController.class)) {
            return true;
        }

        return false;
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest) throws Exception {
        mavContainer.setRequestHandled(true);

        Map<String, Object> resultMap = new HashMap<>();

        resultMap.put("statusCode", STATUS_CODE_SUCCEEDED);
        resultMap.put("data", returnValue);

        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

        messageConverter.write(resultMap, MediaType.APPLICATION_JSON_UTF8, new ServletServerHttpResponse(response));
    }

    private static final String STATUS_CODE_SUCCEEDED = "200";
    private static final String STATUS_CODE_INTERNAL_ERROR = "500";
}

然而在測試過程中,發現Rest的請求並未執行這個Handler!最終通過分析原始碼,發現Spring Boot本身在RequestMappingHandlerAdapter中註冊了一系列的Handler:

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
        List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();

        // Single-purpose return value types
        handlers.add(new ModelAndViewMethodReturnValueHandler());
        handlers.add(new ModelMethodProcessor());
        handlers.add(new ViewMethodReturnValueHandler());
        handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
                this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
        handlers.add(new StreamingResponseBodyReturnValueHandler());
        handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
                this.contentNegotiationManager, this.requestResponseBodyAdvice));
        handlers.add(new HttpHeadersReturnValueHandler());
        handlers.add(new CallableMethodReturnValueHandler());
        handlers.add(new DeferredResultMethodReturnValueHandler());
        handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));

        // Annotation-based return value types
        handlers.add(new ModelAttributeMethodProcessor(false));
        handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
                this.contentNegotiationManager, this.requestResponseBodyAdvice));

        // Multi-purpose return value types
        handlers.add(new ViewNameMethodReturnValueHandler());
        handlers.add(new MapMethodProcessor());

        // Custom return value types
        if (getCustomReturnValueHandlers() != null) {
            handlers.addAll(getCustomReturnValueHandlers());
        }

        // Catch-all
        if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
            handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
        }
        else {
            handlers.add(new ModelAttributeMethodProcessor(true));
        }

        return handlers;
    }

其中關鍵的就是:RequestResponseBodyMethodProcessor,它用於處理REST介面時的返回資料;在新增的時候是先新增這個Processor的,然後才新增getCustomReturnValueHandlers這個方法返回的ValueHandler的。而我們通過WebMvcConfigurer新增進去的ValueHandler是在這個方法裡面返回的。而不同的ValueHandler之間又不能通過Order來進行控制,先執行的如果處理過了就不會再執行後續的了。因此只能採取另外的方式進行:一是通過自定義註解的方式;二是通過修改RequestMappingHandlerAdapter中的returnValueHandlers中的值,將RequestResponseBodyMethodProcessor替換成自定義物件。為儘量不變動Controller的開發,此處採用第二種方式進行。

先定義一個RequestResponseBodyMethodProcessor的包裝類:

public class HandlerMethodReturnValueHandlerProxy implements HandlerMethodReturnValueHandler {
    private HandlerMethodReturnValueHandler proxyObject;

    public HandlerMethodReturnValueHandlerProxy(HandlerMethodReturnValueHandler proxyObject) {
        this.proxyObject = proxyObject;
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return proxyObject.supportsReturnType(returnType);
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest) throws Exception {
        Map<String, Object> resultMap = new HashMap<>();

        resultMap.put("statusCode", STATUS_CODE_SUCCEEDED);
        resultMap.put("errorMessage", "");
        resultMap.put("data", returnValue);

        proxyObject.handleReturnValue(resultMap, returnType, mavContainer, webRequest);
    }

    private static final String STATUS_CODE_SUCCEEDED = "200";
}

然後通過InitializingBean的方式來修改其屬性:

@Configuration
public class RestReturnValueHandlerConfigurer implements InitializingBean {
    @Autowired
    private RequestMappingHandlerAdapter handlerAdapter;

    @Override
    public void afterPropertiesSet() throws Exception {
        List<HandlerMethodReturnValueHandler> list = handlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newList = new ArrayList<>();
        if (null != list) {
            for (HandlerMethodReturnValueHandler valueHandler: list) {
                if (valueHandler instanceof RequestResponseBodyMethodProcessor) {
                    HandlerMethodReturnValueHandlerProxy proxy = new HandlerMethodReturnValueHandlerProxy(valueHandler);
                    newList.add(proxy);
                } else {
                    newList.add(valueHandler);
                }
            }
        }

        handlerAdapter.setReturnValueHandlers(newList);
    }
}

經過這兩步就可以了。

2. 執行出現異常時的處理

可以通過ExceptionHandler來進行處理。其實現如下:

@ControllerAdvice
public class ExceptionHandlerAdvice {

    /**
     * 處理Rest介面請求時的異常
     * @param request
     * @param response
     * @param ex
     * @return
     */
    @ExceptionHandler(RestException.class)
    @ResponseBody
    public Map<String, Object> restError(HttpServletRequest request, HttpServletResponse response, Exception ex) {
        RestException restException = (RestException) ex;
        Map<String, Object> map = new HashMap<>();
        map.put("exception", null != restException.getT() ? restException.getT() : restException);
        map.put("errorMessage", restException.getMessage());
        map.put("url", request.getRequestURL());
        map.put("statusCode",  restException.getCode());
        return map;
    }
}

相關文章