1. 程式人生 > >【Spring實戰(第四版)筆記】——REST傳送錯誤資訊給客戶端

【Spring實戰(第四版)筆記】——REST傳送錯誤資訊給客戶端

<Spring實戰(第四版)筆記>——REST傳送錯誤資訊給客戶端

情景描述:客戶端傳入id,服務端查詢物件並返回資訊。

@RestController
@RequestMapping(value = "city")
public class CityRestController {

    @Autowired
    private CityService cityService;
    @RequestMapping(value = "/api/{id}", method = RequestMethod.GET)
    public City findOneCity(@PathVariable
("id") Long id) { return cityService.findCityById(id); } }

如果根據給定的ID,無法找到某個city物件的ID屬效能夠與之匹配,findOneCity()方法返回null的時候,你覺得會發生什麼呢?
結果就是findOneCity()方法會返回null,響應體為空,不會返回任何有用的資料給客戶端。同時,響應中預設的HTTP狀態碼是200(OK),表示所有的事情執行正常。
但是,所有的事情都是不對的。客戶端要求City物件,但是它什麼都沒有得到。它既沒有收到City物件也沒有收到任何訊息表明出現了錯誤。
伺服器實際上是在說:“這是一個沒用的響應,但是能夠告訴你一切都正常!”
現在,我們考慮一下在這種場景下應該發生什麼。至少,狀態碼不應該是200,而應該是404(Not Found),告訴客戶端它們所要求的內容沒有找到。如果響應體中能夠包含錯誤資訊而不是空的話就更好了。

Spring提供了多種方式來處理這樣的場景:
** 使用@ResponseStatus註解可以指定狀態碼;**
** 控制器方法可以返回ResponseEntity物件,該物件能夠包含更多響應相關的元資料;**
** 異常處理器能夠應對錯誤場景,這樣處理器方法就能關注於正常的狀況。**

使用ResponseEntity,處理返回的狀態碼

作為@ResponseBody的替代方案,控制器方法可以返回一個ResponseEntity物件。
ResponseEntity中可以包含響應相關的元資料(如頭部資訊和狀態碼)以及要轉換成資源表述的物件。
因為ResponseEntity允許我們指定響應的狀態碼,所以當無法找到City物件的時候,我們可以返回HTTP 404錯誤。
如下是新版本的findOneCity(),它會返回ResponseEntity:

/**
 * @author gucailiang
 * @date 2018/10/10
 */
@Controller
@RequestMapping(value = "city")
public class CityController {
    @Autowired
    private CityService cityService;

    /**
     * ResponseEntity中可以包含響應相關的元資料(如頭部資訊和狀態碼)以及要轉換成資源表述的物件。
     * ResponseEntity允許我們指定響應的狀態碼,所以當無法找到City的時候,我們可以返回HTTP 404錯誤
     * <p>
     * 路徑中得到的ID用來從Repository中檢索City。如果找到的話,狀態碼設定
     * 為HttpStatus.OK(這是之前的預設值),但是如果Repository返回null的話,狀態碼設定為HttpStatus.NOT_FOUND,這會轉換為HTTP 404。
	 * 最後,會建立一個新的ResponseEntity,它會把City和狀態碼傳送給客戶
     * <p>
     * ResponseEntity還包含了@ResponseBody的語義,因此負載部分將會渲染到響應體中,就像之前在方法上使用@ResponseBody註解一樣。
	 * 如果返回ResponseEntity的話,那就沒有必要在方法上使用@ResponseBody註解了。
     * </p>
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/api2/{id}", method = RequestMethod.GET)
    public ResponseEntity<City> findOneCityById(@PathVariable("id") Long id) {
        City city = cityService.findCityById(id);
        HttpStatus status = city != null ? HttpStatus.OK : HttpStatus.NOT_FOUND;
        return new ResponseEntity<City>(city, status);
    }

}

注意這個CityById()方法沒有使用@ResponseBody註解。除了包含響應頭資訊、狀態碼以及負載以外,ResponseEntity還包含了@ResponseBody的語義,因此負載部分將會渲染到響應體中,就像之前在方法上使用@ResponseBody註解一樣。如果返回ResponseEntity的話,那就沒有必要在方法上使用@ResponseBody註解了。

使用ResponseEntity並在響應體中返回錯誤資訊

我們走出了第一步,如果所要求的City無法找到的話,客戶端能夠得到一個合適的狀態碼。
但是在上個例中,響應體依然為空。我們可能會希望在響應體中包含一些錯誤資訊。

/**
 * @author gucailiang
 * @date 2018/10/10
 */
public class Error {
    private int code;
    private String message;

    public Error(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}


@Controller
@RequestMapping(value = "city")
public class CityController {
    /**
     * 返回響應體包含錯誤資訊
     * @param id
     * @return
     */
    @RequestMapping(value = "/api3/{id}", method = RequestMethod.GET)
    public ResponseEntity<?> findOneCity3(@PathVariable("id") Long id) {
        City city = cityService.findCityById(id);
        if(null == city){
            Error error = new Error(4, "city id [" + id + "] not found");
            return new ResponseEntity<Error>(error,HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<City>(city, HttpStatus.OK);
    }
}

如果找到City 的話,就會把返回的物件以及200(OK)的狀態碼封裝到ResponseEntity中。另一方面,如果findCityById()返回null的話,將會建立一個Error物件,並將其與404(Not Found)狀態碼一起封裝到ResponseEntity中,然後返回。

更進一步,使用異常處理器

首先,這比我們開始的時候更為複雜。涉及到了更多的邏輯,包括條件語句。
另外,方法返回ResponseEntity<?>感覺有些問題。ResponseEntity所使用的泛型為它的解析或出現錯誤留下了太多的空間。

泛型參考

<? super T>表示包括T在內的任何T的父類,
<? extends T>表示包括T在內的任何T的子類
可以解決當具體型別不確定的時候,這個萬用字元就是 ?

不過,我們可以藉助錯誤處理器來修正這些問題。
方法中的if程式碼塊是處理錯誤的,但這是控制器中錯誤處理器(errorhandler)所擅長的領域。
錯誤處理器能夠處理導致問題的場景,這樣常規的處理器方法就能只關心正常的邏輯處理路徑了

@Controller
@RequestMapping(value = "city")
public class CityController {
    @Autowired
    private CityService cityService;
    /**
     * 使用異常處理器處理異常
     * 這個版本的CityById()方法確實乾淨了很多。除了對返回值進行null檢查,它完全關注於成功的場景,也就是能夠找到請求的City。同時,在返回型別中,我們能移除掉奇怪的泛型了。
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/api4/{id}", method = RequestMethod.GET)
    public ResponseEntity<City> findOneCity4(@PathVariable("id") Long id) {
        City city = cityService.findCityById(id);
        if (null == city) {
            throw new CityNotFoundException(id);
        }
        return new ResponseEntity<City>(city, HttpStatus.OK);
    }

    /**
     * 對應CityNotFoundException的錯誤處理器
     * <p>
     *
     * @ExceptionHandler註解能夠用到控制器方法中,用來處理特定的異常。
     * 這裡,它表明如果在控制器的任意處理方法中丟擲CityNotFoundException異常,就會呼叫cityNotFound()方法來處理異常。 
     * </p>
     */
    @ExceptionHandler(CityNotFoundException.class)
    public ResponseEntity<Error> cityNotFound(CityNotFoundException e) {
        long cityId = e.getCityId();
        Error error = new Error(4, "city id [" + cityId + "] not found");
        return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
    }
}

public class CityNotFoundException extends RuntimeException {
    private long cityId;

    public CityNotFoundException(long cityId) {
        this.cityId = cityId;
    }

    public long getCityId() {
        return cityId;
    }
}

@ExceptionHandler註解能夠用到控制器方法中,用來處理特定的異常。這裡,它表明如果在控制器的任意處理方法中丟擲CityNotFoundException異常,就會呼叫CityNotFound()方法來處理異常。
這個版本的findCityById()方法確實乾淨了很多。除了對返回值進行null檢查,它完全關注於成功的場景,也就是能夠找到請求的City。同時,在返回型別中,我們能移除掉奇怪的泛型了。

乾淨的版本:ResponseEntity已經不需要了,只需要@Responbody

我們能夠讓程式碼更加乾淨一些。現在我們已經知道findCityById()將會返回City並且HTTP狀態碼始終會是200(OK),那麼就可以不再使用ResponseEntity,而是將其替換為@ResponseBody。
當然,如果控制器類上使用了@RestController,我們甚至不再需要@ResponseBody

@Controller
@RequestMapping(value = "city")
public class CityController {
    @Autowired
    private CityService cityService;
    /**
     * 乾淨的版本:ResponseEntity已經不需要了,只需要Responbody
     * 現在我們已經知道spittleById()將會返回Spittle並且HTTP狀態碼始終會是200(OK),那麼就可以不再使用ResponseEntity,而是將其替換為@ResponseBody
     *
     * @param id
     * @return
     */
    @RequestMapping(value = "/api5/{id}", method = RequestMethod.GET)
    public @ResponseBody City findOneCity5(@PathVariable("id") Long id) {
        City city = cityService.findCityById(id);
        if (null == city) {
            throw new CityNotFoundException(id);
        }
        return city;
    }
}

還有沒有清理呢?——清理異常處理器

鑑於錯誤處理器的方法會始終返回Error,並且HTTP狀態碼為404(Not Found),那麼現在我們可以對CityNotFound()方法進行類似的清理:


    /**
     * 鑑於錯誤處理器的方法會始終返回Error,並且HTTP狀態碼為404(Not Found),那麼現在我們可以對spittleNotFound()方法進行類似的清理:
     * <p>
     * 因為spittleNotFound()方法始終會返回Error,所以使用ResponseEntity的唯一原因就是能夠設定狀態碼。
     * 但是通過為spittleNotFound()方法新增@ResponseStatus(HttpStatus.NOT_FOUND)註解,我們可以達到相同的效果,而且可以不再使用ResponseEntity了。
     * </p>
     */
    @ExceptionHandler(CityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public @ResponseBody Error cityNotFound2(CityNotFoundException e) {
        long cityId = e.getCityId();
        return new Error(4, "city id [" + cityId + "] not found");
    }

結語

在一定程度上,我們已經圓滿達到了想要的效果。為了設定響應狀態碼,我們首先使用ResponseEntity,但是稍後我們藉助異常處理器以及@ResponseStatus,避免使用ResponseEntity,從而讓程式碼更加整潔。

似乎,我們不再需要使用ResponseEntity了。但是,有一種場景ResponseEntity能夠很好地完成,但是其他的註解或異常處理器卻做不到。現在,我們看一下如何在響應中設定頭部資訊。
請看下一篇部落格——在響應中設定頭部資訊