前言: 關(guān)于 Spring 的全局處理,我有兩方面要說: 統(tǒng)一數(shù)據(jù)返回格式統(tǒng)一異常處理 為了將兩個問題說明清楚,將分兩個章節(jié)分別說明,本章主要說第一點 有童鞋說,我們項目都做了這種處理,就是在每個 API 都單獨工具類將返回值進行封裝,但這種不夠優(yōu)雅;我想寫最少的代碼完成這件事,也許有童鞋說,加幾個注解就解決問題了,說的沒錯,但這篇文章主要是為了說明為什么加了幾個注解就解決問題了,目的是希望大家知其所以然。
為了更好的說明問題,本文先說明如何實現(xiàn),然后再詳細剖析實現(xiàn)原理(這很關(guān)鍵) 為什么要做統(tǒng)一數(shù)據(jù)返回格式 前后端分離是當今服務形式的主流,如何設(shè)計一個好的 RESTful API ,以及如何讓前端小伙伴可以處理標準的 response JSON 數(shù)據(jù)結(jié)構(gòu)都至關(guān)重要,為了讓前端有更好的邏輯展示與頁面交互處理,每一次 RESTful 請求都應該包含以下幾個信息: 名稱描述status狀態(tài)碼,標識請求成功與否,如 [1:成功;-1:失敗]errorCode錯誤碼,給出明確錯誤碼,更好的應對業(yè)務異常;請求成功該值可為空errorMsg錯誤消息,與錯誤碼相對應,更具體的描述異常信息resultBody返回結(jié)果,通常是 Bean 對象對應的 JSON 數(shù)據(jù), 通常為了應對不同返回值類型,將其聲明為泛型類型 實現(xiàn) 通用返回值類定義 根據(jù)上面的描述,用 Java Bean 來體現(xiàn)這個結(jié)構(gòu)就是這樣: @Data public final class CommonResult { private int status=1; private String errorCode=""; private String errorMsg=""; private T resultBody; public CommonResult() { } public CommonResult(T resultBody) { this.resultBody=resultBody; } } 配置 沒錯,我們需要借助幾個關(guān)鍵注解來完成一下相關(guān)配置: @EnableWebMvc @Configuration public class UnifiedReturnConfig { @RestControllerAdvice("com.example.unifiedreturn.api") static class CommonResultResponseAdvice implements ResponseBodyAdvice{ @Override public boolean supports(MethodParameter methodParameter, Class> aClass) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { if (body instanceof CommonResult){ return body; } return new CommonResult(body); } } } 到這里就結(jié)束了,我們就可以縱情的寫任何 RESTful API 了,所有的返回值都會有統(tǒng)一的 JSON 結(jié)構(gòu) 測試 新建 UserController,添加相應的 RESTful API,測試用例寫的比較簡單,只為了說明返回值的處理 @RestController @RequestMapping("/users") public class UserController { @GetMapping("") public List getUserList(){ List userVoList=Lists.newArrayListWithCapacity(2); userVoList.add(UserVo.builder().id(1L).name("日拱一兵").age(18).build()); userVoList.add(UserVo.builder().id(2L).name("tan").age(19).build()); return userVoList; } } 打開瀏覽器輸入地址測試: localhost:8080/users/ ,我們可以看到返回了 List JSON 數(shù)據(jù)
繼續(xù)添加 RESTful API,根據(jù)用戶 ID 查詢用戶信息 @GetMapping("/{id}") public UserVo getUserByName(@PathVariable Long id){ return UserVo.builder().id(1L).name("日拱一兵").age(18).build(); } 打開瀏覽器輸入地址測試: localhost:8080/users/1 ,我們可以看到返回了單個 User JSON 數(shù)據(jù)
添加一個返回值類型為 ResponseEntity 的 API @GetMapping("/testResponseEntity") public ResponseEntity getUserByAge(){ return new ResponseEntity(UserVo.builder().id(1L).name("日拱一兵").age(18).build(), HttpStatus.OK); } 打開瀏覽器輸入地址測試: localhost:8080/users/testResponseEntity ,我們可以看到同樣返回了單個 User JSON 數(shù)據(jù)
解剖實現(xiàn)過程 我會將關(guān)鍵部分一一說明清楚,斷案還需小伙伴自己去案發(fā)現(xiàn)場(打開自己的 IDE 查看) 故事要從 @EnableWebMvc 這個注解說起,打開該注解看: @Retention(RetentionPolicyTIME) @Target(ElementType.TYPE) @Documented @Import(DelegatingWebMvcConfiguration.class) public @interface EnableWebMvc { } 通過 @Import 注解引入了 DelegatingWebMvcConfiguration.class,那來看這個類吧: @Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { ... } 有 @Configuration 注解,你應該很熟悉了,該類的父類 WebMvcConfigurationSupport 中卻隱藏著一段關(guān)鍵代碼: @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter=createRequestMappingHandlerAdapter(); ... return adapter; } RequestMappingHandlerAdapter 是每一次請求處理的關(guān)鍵,來看該類的定義: public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... } 該類實現(xiàn)了 InitializingBean 接口,我在 Spring Bean 生命周期之“我從哪里來”? 這篇文章中明確說明了 Spring Bean 初始化的幾個關(guān)鍵,其中 InitializingBean 接口的 afterPropertiesSet 方法就是關(guān)鍵之一,在 RequestMappingHandlerAdapter 類中同樣重寫了該方法: @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans initControllerAdviceCache(); if (this.argumentResolvers==null) { List resolvers=getDefaultArgumentResolvers(); this.argumentResolvers=new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.initBinderArgumentResolvers==null) { List resolvers=getDefaultInitBinderArgumentResolvers(); this.initBinderArgumentResolvers=new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers==null) { List handlers=getDefaultReturnValueHandlers(); this.returnValueHandlers=new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } 該方法內(nèi)容都非常關(guān)鍵,但我們先來看 initControllerAdviceCache 方法,其他內(nèi)容后續(xù)再單獨說明: private void initControllerAdviceCache() { ... if (logger.isInfoEnabled()) { logger("Looking for @ControllerAdvice: " + getApplicationContext()); } List beans=ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); AnnotationAwareOrderComparator.sort(beans); List requestResponseBodyAdviceBeans=new ArrayList(); for (ControllerAdviceBean bean : beans) { ... if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) { requestResponseBodyAdviceBeans.add(bean); } } } 通過 ControllerAdviceBean 靜態(tài)方法掃描 ControllerAdvice 注解,可是我們在實現(xiàn)上使用的是 @RestControllerAdvice注解,打開看該注解: @Target(ElementType.TYPE) @Retention(RetentionPolicyTIME) @Documented @ControllerAdvice @ResponseBody public @interface RestControllerAdvice { 該注解由 @ControllerAdvice 和 @ResponseBody 標記,就好比你熟悉的 @RestController 注解由 @Controller 和 @ResponseBody 標記是一樣的 到這里你已經(jīng)知道我們用 @RestControllerAdvice 標記的 Bean 是如何被加載到 Spring 上下文的,接下來就要知道是 Spring 是如何使用我們的 bean 以及對返回 body 做處理的 其實在 HttpMessageConverter是如何轉(zhuǎn)換辦統(tǒng)招文憑數(shù)據(jù)的? 這篇文章中已經(jīng)說明了一部分,希望小伙伴先看這篇文章,下面的部分就會秒懂了,我們在這里做進一步的說明 在 AbstractMessageConverterMethodProcessor 的 writeWithMessageConverters 方法中,有一段核心代碼: if (messageConverter instanceof GenericHttpMessageConverter) { if (((GenericHttpMessageConverter) messageConverter).canWrite( declaredType, valueType, selectedMediaType)) { outputValue=(T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class>) messageConverter.getClass(), inputMessage, outputMessage); ... return; } } 可以看到通過 getAdvice() 調(diào)用了 beforeBodyWrite 方法,我們已經(jīng)接近真相了 protected RequestResponseBodyAdviceChain getAdvice() { return this.advice; } RequestResponseBodyAdviceChain,看名字帶有 Chain,很明顯用到了「責任鏈設(shè)計模式」,這些內(nèi)容在 不得不知的責任鏈設(shè)計模式 文章中明確說明過,只不過它傳遞責任鏈以循環(huán)的方式完成: class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice { @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType, Class> converterType, ServerHttpRequest request, ServerHttpResponse response) { return processBody(body, returnType, contentType, converterType, request, response); } @SuppressWarnings("unchecked") private Object processBody(Object body, MethodParameter returnType, MediaType contentType, Class> converterType, ServerHttpRequest request, ServerHttpResponse response) { for (ResponseBodyAdvice advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { if (advice.supports(returnType, converterType)) { body=((ResponseBodyAdvice) advice).beforeBodyWrite((T) body, returnType, contentType, converterType, request, response); } } return body; } } 我們重寫的 beforeBodyWrite 方法終究會被調(diào)用到,真相就是這樣了!!! 其實還沒完,你有沒有想過,如果我們的 API 方法返回值是 org.springframework.http.ResponseEntity 類型,我們可以指定 HTTP 返回狀態(tài)碼,但是這個返回值會直接放到我們的 beforeBodyWrite 方法的 body 參數(shù)中嗎?如果這樣做很明顯是錯誤的,因為 ResponseEntity 包含很多我們非業(yè)務數(shù)據(jù)在里面,那 Spring 是怎么幫我們處理的呢? 在我們方法取得返回值并且在調(diào)用 beforeBodyWrite 方法之前,還要選擇 HandlerMethodReturnValueHandler 用于處理不同的 Handler 來處理返回值 在類 HandlerMethodReturnValueHandlerComposite 中的 handleReturnValue 方法中 @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { HandlerMethodReturnValueHandler handler=selectHandler(returnValue, returnType); if (handler==null) { throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName()); } handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } 通過調(diào)用 selectHandler 方法來選擇合適的 handler,Spring 內(nèi)置了很多個 Handler,我們來看類圖:
HttpEntityMethodProcessor 就是其中之一,它重寫了 supportsParameter 方法,支持 HttpEntity 類型,即支持 ResponseEntity 類型: @Override public boolean supportsParameter(MethodParameter parameter) { return (HttpEntity.class==parameter.getParameterType() || RequestEntity.class==parameter.getParameterType()); } 所以當我們返回的類型為 ResponseEntity 時,就要通過 HttpEntityMethodProcessor 的 handleReturnValue 方法來處理我們的結(jié)果: @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { ... if (responseEntity instanceof ResponseEntity) { int returnStatus=((ResponseEntity) responseEntity).getStatusCodeValue(); outputMessage.getServletResponse().setStatus(returnStatus); if (returnStatus==200) { if (SAFE_METHODS.contains(inputMessage.getMethod()) && isResourceNotModified(inputMessage, outputMessage)) { // Ensure headers are flushed, no body should be written. outputMessage.flush(); // Skip call to converters, as they may update the body. return; } } } // Try even with null body. ResponseBodyAdvice could get involved. writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage); // Ensure headers are flushed even if no body was written. outputMessage.flush(); } 該方法提取出 responseEntity.getBody(),并傳遞個 MessageConverter,然后再繼續(xù)調(diào)用 beforeBodyWrite 方法,這才是真相!!! 這是 RESTful API 正常返回內(nèi)容的情況。 |
|
來自: 新用戶248587GZ > 《待分類》