基於 SpringMVC 的 RESTful HTTP API 實踐(服務端)

alexpdh's blog發表於2017-06-29

理解 REST

REST(Representational State Transfer),中文翻譯叫“表述性狀態轉移”。是 Roy Thomas Fielding 在他2000年的博士論文中提出的。它與傳統的 SOAP Web 服務區別在於,REST關注的是要處理的資料,而 SOAP 主要關注行為和處理。要理解好 REST,根據其首字母拆分出的英文更容易理解。

表述性(Representational):對於 REST 來說,我們網路上的一個個URI資源可以用各種形式來表述,例如:XML、JSON或者HTML等。

狀態(State):REST 更關注資源的狀態而不是對資源採取的行為。

轉移(Transfer):在網路傳輸過程中,REST 使資源以某種表述性形式從一個應用轉移到另一個應用(如從服務端轉移到客戶端)。

具體來說,REST 中存在行為,它的行為是通過 HTTP 表示操作的方法來定義的即:GET、POST、PUT、DELETE、PATCH;GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源,PATCH用來更新資源。 基於 REST 這樣的觀點,我們需要避免使用 REST服務、REST Web服務 這樣的稱呼,這些稱呼多少都帶有一些強調行為的味道。

使用 RESTful 架構設計使用誤區

RESTful 架構:是基於 REST 思想的時下比較流行的一種網際網路軟體架構。它結構清晰、符合標準、易於理解、擴充套件方便,所以正得到越來越多網站的採用。

在沒有足夠了解 REST 的時候,我們很容易錯誤的將其視為 “基於 URL 的 Web 服務”,即將 REST 和 SOAP 一樣,是一種遠端過程呼叫(remote procedure call,RPC)的機制。但是 REST 和 RPC 幾乎沒有任何關係,RPC 是面向服務的,而 REST 是面向資源的,強調描述應用程式的事物和名詞。這樣很容易導致的一個結果是我們在設計 RESTful API 時,在 URI 中使用動詞。例如:GET /user/getUser/123。正確寫法應該是 GET /user/123。

使用 springMVC 支援 RESTful

在 spring 3.0 以後,spring 這對 springMVC 的一些增強功能對 RESTful 提供了良好的支援。在4.0後的版本中,spring 支援一下方式建立 REST 資源:

  1. 控制器可以處理所有的 HTTP 方法,包含幾個主要的 REST 方法:GET、POST、PUT、DELETE、PATCH;
  2. 藉助 @PathVariable 註解,控制器能夠處理引數化的 URL(將變數輸入作為 URL 的一部分);
  3. 藉助 spring 的檢視解析器,資源能夠以多種方式進行表述,包括將模型資料渲染為 XML、JSON、Atom、已經 RSS 的 View 實現;
  4. 可以使用 ContentNegotiatingViewResolver 來選擇最適合客戶端的表述;
  5. 藉助 @ResponseBody 註解和各種 HttpMethodConverter 實現,能夠替換基於檢視的渲染方式;
  6. 類似地,@RequestBody 註解以及 HttpMethodConverter 實現可以將傳入的 HTTP 資料轉化為傳入控制器處理方法的 Java 物件;
  7. 藉助 RestTemplate ,spring 應用能夠方便地使用 REST 資源。

建立 RESTful 控制器

程式碼清單

package com.pengdh.controller;
import com.pengdh.entity.EmployeeEntity;
import com.pengdh.service.EmployeeService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
 * @author pengdh
 * @date: 2017-06-27 0:08
 */
@Controller
@RequestMapping("/employs")
public class EmployeeController {
  @Autowired
  private EmployeeService empService;
  @RequestMapping(value = "/list", method = RequestMethod.GET, produces = { "application/json;charset=UTF-8" })
  public List<EmployeeEntity> employs(Integer offset,Integer limit) {
    offset = offset == null ? 0 : offset;
    limit = limit == null ? 20 : limit;
    return empService.queryEmployList(offset,limit);
  }
}

程式碼的大致過程是當客戶端發起對 “/employs” 的 GET 請求時,將呼叫服務端的 employs 方法,服務端通過注入的 EmployeeService 獲取到一個 EmployeeEntity 列表,並將列表以 JSON 的表述形式返回給客戶端。

  • 需要注意的是這裡控制器本身並不關心資源如何表述。控制器以 Java 物件的方式來處理資源。控制器完成了它的工作以後,資源才會被轉化成為適合客戶端的形式。spring 提供了兩種方法將資源的 java 表述形式轉化為傳送給客戶端的表述形式:
    • 內容協商(Content negotiation):選擇一個檢視,它能夠將模型渲染為呈現給客戶端的表述形式;
    • 訊息轉化器(Message conversion):通過一個訊息轉換器將控制器所返回的物件轉換為呈現給客戶端的表述形式。

對於上述兩種方式,第一種方式是通過 ContentNegotiatingViewResolver 作為 ViewResolver 的實現,主要是用於將資源渲染人類使用者介面所需要的檢視模型,如:HTML、JSP等也可以渲染。也可以針對不是人類客戶端產生 JSON 或 XML,但是效果不是很理想,往往會產生一些不是客戶端所需要的預期結果。如:客戶端希望得到的響應可能是:{“name”:”zhangs”,”age”:”20”}。而模型是 key-value 組成的 map ,可能最終的響應是這樣的:{“user”:{“name”:”zhangs”,”age”:”20”}}。基於內容協商的這些限制,這裡我們主要討論第二種方式:使用 Spring 的訊息轉換功能來生成資源表述。

使用 HTTP 訊息轉換器

這是一種更為直接的方式,訊息轉換器能夠將控制器產生的資料轉換為服務於客戶端的表述形式。常用的一些訊息轉換器如:Jackson 的 MappingJacksonHttpMessageConverter 實現 JSON 訊息和 Java 物件的互相轉換; JAXB 庫的 Jaxb2RootElementHttpMessageConverter 實現 XML 和 Java 物件的相互轉換等。

通過 @ResponseBody 註解實現響應體中返回資源狀態。

正常情況下,當處理方法返回 Java 物件時,這個物件會放在模型中並在檢視中渲染使用。但是,如果使用了訊息轉換功能的話,我們需要告訴 Spring 跳過正常的模型/檢視流程,並使用訊息轉換器。實現這種方式最簡單的方式是在控制器的方法上新增 @ResponseBody 註解。如:

@RequestMapping(value = "/list", method = RequestMethod.GET, produces = { "application/json;charset=UTF-8" })
@ResponseBody
public List<EmployeeEntity> employs(Integer offset,Integer limit) {
  offset = offset == null ? 0 : offset;
  limit = limit == null ? 20 : limit;
  return empService.queryEmployList(offset,limit);
}

這裡 @ResponseBody 註解會告知 Spring 將 List 轉換成 JSON 這樣的表述形式作為資源傳送給客戶端。

使用 @RequestBody 註解實現在請求體中接收資源狀態

使用 @RequestBody 註解可以告知 Spring 查詢一個訊息轉換器,將來自客戶端的資源表述轉換為物件。如:

@RequestMapping(value = "/save", method = RequestMethod.POST, produces = { "application/json;charset=UTF-8" })
  public int saveEmploy(@RequestBody EmployeeEntity employeeEntity) {
    return empService.save(employeeEntity);
  }

使用 @RestController 註解為控制器預設設定訊息轉換

Spring 4.0 引入了 @RestController 註解,在控制器是用 @RestController 代替 @Controller 的話,Spring 將會為該控制器的所有處理方法應用訊息轉換功能。我們不必在每個方法都新增 @ResponseBody 註解了。如:

package com.pengdh.controller;
import com.pengdh.entity.EmployeeEntity;
import com.pengdh.service.EmployeeService;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author pengdh
 * @date: 2017-06-27 0:08
 */
@RestController
@RequestMapping("/employs")
public class EmployeeController {
  @Autowired
  private EmployeeService empService;
  @RequestMapping(value = "/list", method = RequestMethod.GET, produces = { "application/json;charset=UTF-8" })
  public List<EmployeeEntity> employs(Integer offset,Integer limit) {
    offset = offset == null ? 0 : offset;
    limit = limit == null ? 20 : limit;
    return empService.queryEmployList(offset,limit);
  }
  @RequestMapping(value = "/save", method = RequestMethod.POST, produces = { "application/json;charset=UTF-8" })
  public int saveEmploy(@RequestBody EmployeeEntity employeeEntity) {
    return empService.save(employeeEntity);
  }
}

為客戶端提供其他後設資料

使用 ResponseEntity 提供更多響應相關的後設資料

可以利用 ResponseEntity 給客戶端返回狀態碼、設定響應頭資訊等,如給客戶端提供返回碼:

@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = { "application/json;charset=UTF-8" })
 public ResponseEntity<EmployeeEntity> employById(@PathVariable long id) {
   HttpStatus status = null;
   EmployeeEntity employeeEntity = empService.selectById(id);
   if (employeeEntity != null) {
     status = HttpStatus.OK;
   } else {
     status = HttpStatus.NOT_FOUND;
   }
   return new ResponseEntity<EmployeeEntity>(employeeEntity, status);
 }

如果沒有 if 判斷,當根據 id 找不到對應的資訊的時候,返回給客戶端的狀態碼是預設的 HttpStatus.OK;當加上了判斷條件後如果沒有相應的資訊返回則設定返回狀態碼為 HttpStatus.NOT_FOUND,最後通過 new 一個 ResponseEntity 會將查詢資訊和狀態碼一起返回到客戶端。

另外,ResponseEntity 還包含有 @ResponseBody 的語義,上面示例中並沒有使用 @ResponseBody 註解,但是 ResponseEntity 的負載部分同樣可以渲染到響應體中。

使用控制器異常處理器 @ExceptionHandler 處理異常資訊

@ExceptionHandler 可以用到控制器的方法中,處理特定的異常:

建立響應包裝類 ResponseResult

package com.pengdh.dto;
import java.io.Serializable;
/**
 * 響應結果封裝類
 *
 * @author pengdh
 * @date: 2017-06-29 0:34
 */
public class ResponseResult<T> implements Serializable {
  private static final long serialVersionUID = -3371934618173052904L;
  private String code;
  private String desc;
  private T data;
  public ResponseResult() {
  }
  public ResponseResult(String code, String desc) {
    this.code = code;
    this.desc = desc;
  }
  public ResponseResult(String code, T data) {
    this.code = code;
    this.data = data;
  }
  public String getCode() {
    return code;
  }
  public void setCode(String code) {
    this.code = code;
  }
  public String getDesc() {
    return desc;
  }
  public void setDesc(String desc) {
    this.desc = desc;
  }
  public T getData() {
    return data;
  }
  public void setData(T data) {
    this.data = data;
  }
  @Override
  public String toString() {
    return "ResponseResult{" +
        "code='" + code + '\'' +
        ", desc='" + desc + '\'' +
        ", data=" + data +
        '}';
  }
}

建立一個異常類 ResourceNotFound

package com.pengdh.exception;
/**
 * 資源未找到異常
 *
 * @author pengdh
 * @date: 2017-06-29 0:55
 */
public class ResourceNotFound extends RuntimeException {
  private static final long serialVersionUID = 4880328265878141724L;
  public ResourceNotFound() {
    super();
  }
  public ResourceNotFound(String message) {
    super(message);
  }
  public ResourceNotFound(String message, Throwable cause) {
    super(message, cause);
  }
}

控制器 EmployeeController

package com.pengdh.controller;
import com.pengdh.dto.ResponseResult;
import com.pengdh.entity.EmployeeEntity;
import com.pengdh.exception.ResourceNotFound;
import com.pengdh.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author pengdh
 * @date: 2017-06-27 0:08
 */
@RestController
@RequestMapping("/employs")
public class EmployeeController {
  @Autowired
  private EmployeeService empService;
  @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = { "application/json;charset=UTF-8" })
  public ResponseResult<EmployeeEntity> employById(@PathVariable long id) {
    ResponseResult<EmployeeEntity> result = new ResponseResult<EmployeeEntity>();
    HttpStatus status = null;
    EmployeeEntity employeeEntity = empService.selectById(id);
    if (employeeEntity == null) {
      throw new ResourceNotFound(String.valueOf(id));
    }
    result.setCode(String.valueOf(HttpStatus.OK));
    result.setData(employeeEntity);
    return result;
  }
  @ExceptionHandler(ResourceNotFound.class)
  public ResponseResult<Object> handlerException(ResourceNotFound e) {
    ResponseResult<Object> result = new ResponseResult<Object>();
    result.setCode(String.valueOf(HttpStatus.NOT_FOUND));
    result.setDesc(e.getMessage());
    return result;
  }
}

從控制器程式碼可以看出,我們通過 @ExceptionHandler 能將控制器的方法的異常場景分出來單獨處理。

參考文獻

相關文章