一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

每天用SpringBoot,還不懂RESTfulAPI返回統(tǒng)一數(shù)據(jù)格式怎么實現(xiàn)?

 新用戶248587GZ 2022-02-14

  前言:

  關(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ù)

  每天用SpringBoot,還不懂RESTfulAPI返回統(tǒng)一數(shù)據(jù)格式怎么實現(xiàn)?

  繼續(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ù)

  每天用SpringBoot,還不懂RESTfulAPI返回統(tǒng)一數(shù)據(jù)格式怎么實現(xiàn)?

  添加一個返回值類型為 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ù)

  每天用SpringBoot,還不懂RESTfulAPI返回統(tǒng)一數(shù)據(jù)格式怎么實現(xiàn)?

  解剖實現(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,我們來看類圖:

  每天用SpringBoot,還不懂RESTfulAPI返回統(tǒng)一數(shù)據(jù)格式怎么實現(xiàn)?

  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)容的情況。

    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    好吊色欧美一区二区三区顽频| 一本色道久久综合狠狠躁| 国内精品一区二区欧美| 国产又粗又爽又猛又黄的| 欧美午夜色视频国产精品| 亚洲男人的天堂就去爱| 国产一区二区三区口爆在线| 亚洲清纯一区二区三区| 一个人的久久精彩视频| 久久91精品国产亚洲| 久热青青草视频在线观看| 韩国日本欧美国产三级| 欧美日韩精品久久亚洲区熟妇人 | 日韩精品在线观看一区| 久久中文字幕中文字幕中文| 久久精品国产99精品最新| 91欧美一区二区三区| 免费在线成人激情视频| 国产欧美日韩精品一区二| 亚洲国产成人一区二区在线观看| 国产在线视频好看不卡| 一区中文字幕人妻少妇| 黑鬼糟蹋少妇资源在线观看| 亚洲成人精品免费在线观看| 99久久精品午夜一区二| 成人午夜视频在线播放| 亚洲欧美精品伊人久久| 亚洲高清亚洲欧美一区二区| 麻豆一区二区三区在线免费| 国产欧美日本在线播放| 久久碰国产一区二区三区| 99国产精品国产精品九九| 国产精品国产亚洲看不卡| 欧美国产极品一区二区| 国产一区二区精品高清免费| 亚洲欧美国产精品一区二区| 人妻精品一区二区三区视频免精| 国产无摭挡又爽又色又刺激| 午夜亚洲少妇福利诱惑| 亚洲另类欧美综合日韩精品| 亚洲超碰成人天堂涩涩|