利用 Spring Boot 設(shè)計(jì)風(fēng)格良好的Restful API及錯(cuò)誤響應(yīng)

一、前言

網(wǎng)上經(jīng)常會(huì)看到一些文章,旨在介紹如何使用Spring MVC或Spring Boot實(shí)現(xiàn)Restful接口,譬如:

 @RequestMapping(value = "/addUser", method = RequestMethod.POST)
    public boolean addUser( User user) {
        System.out.println("開始新增...");
        return userService.addUser(user);
    }
    
    @RequestMapping(value = "/updateUser", method = RequestMethod.PUT)
    public boolean updateUser( User user) {
        System.out.println("開始更新...");
        return userService.updateUser(user);
    }
    
    @RequestMapping(value = "/deleteUser", method = RequestMethod.DELETE)
    public boolean delete(@RequestParam(value = "userName", required = true) int userId) {
        System.out.println("開始刪除...");
        return userService.deleteUser(userId);
    }
    
    @RequestMapping(value = "/userId", method = RequestMethod.GET)
    public User findByUserId(@RequestParam(value = "userId", required = true) int userId) {
        System.out.println("開始查詢...");
        return userService.findUserById(userId);
    }

對(duì)于如上實(shí)現(xiàn)方式,本人著實(shí)不敢恭維。試問,這算哪門子的Restful?自認(rèn)為其與Restful無絲毫關(guān)系,有誤導(dǎo)眾人之嫌。

對(duì)于RESTful 的相關(guān)概念,以及其API的設(shè)計(jì)方法,本人極力推薦阮一峰大神的文章《RESTful API 設(shè)計(jì)指南》,僅此一篇足夠已。在此基礎(chǔ)上,本人將小試牛刀,介紹如何在Spring Boot項(xiàng)目中設(shè)計(jì)風(fēng)格良好的Restful API,以及如何實(shí)現(xiàn)Restful的錯(cuò)誤響應(yīng)。

文筆拙劣,并且水平有限,望各位看官不吝賜教,相互交流~

二、項(xiàng)目介紹

本項(xiàng)目IDE使用 intellij idea 2018, 構(gòu)建工具使用Maven,JDK使用1.8。方便起見,我們可以使用maven的原型插件maven-archetype-quickstart快速建立一個(gè)Java 工程,在此基礎(chǔ)上再進(jìn)行功能開發(fā)。

2.1 目錄結(jié)構(gòu)

image.png

如上目錄結(jié)構(gòu),了解Spring Boot或Spring MVC開發(fā)的朋友應(yīng)該再熟悉不過了。這是一個(gè)較簡(jiǎn)單用戶(User)服務(wù),目前只實(shí)現(xiàn)了對(duì)用戶模型基本的增刪改查功能,尚未考慮多種異常情況。

2.2 項(xiàng)目依賴

在pom.xml中引入如下依賴:

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>1.5.14.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <version>1.5.14.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
    </dependencies>

由上可知,我們分別引入了如下依賴:

  • spring-boot-starter-web
    眾所周知,這是使用spring boot做web開發(fā)的必備依賴
  • spring-boot-starter-data-jpa
    本項(xiàng)目使用JPA作為ORM框架
  • springfox-swagger2與springfox-swagger-ui
    swagger是個(gè)好東西,可以用來生成RESTFUL接口的在線文檔,而且更牛逼的是可以直接在文檔中進(jìn)行接口測(cè)試,代替Postman。在Spring Boot工程中,可以引入這兩個(gè)依賴實(shí)現(xiàn)swagger的眾多功能。
  • mysql-connector-java
    不必多言,使用mysql必備

2.3 Spring Boot 配置文件

resources目錄中定義配置文件application.properties:

spring.jpa.database=MySQL
spring.datasource.url=jdbc:mysql://*.*.*.*:3306/test
spring.datasource.username=root
spring.datasource.password=abc

server.port=8801

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jackson.serialization.indent_output=true

logging.level.root=info

注意,除此之外,在pom.xml的build節(jié)點(diǎn)中,還需指定resources的路徑:

        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>

2.4 模型定義

模型 User 相當(dāng)簡(jiǎn)單,只有id、userName、age三個(gè)屬性。其中,id我們不使用自增主鍵,直接利用JPA提供的UUID主鍵生成策略,如下所示:

//User.java
package com.mystudy.spring.domain;

import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.NotNull;

@GenericGenerator(name = "jpa-uuid", strategy = "uuid")
@Entity
public class User
{
    @Id
    @NotNull
    @GeneratedValue(generator = "jpa-uuid")
    private String id;

    @NotNull
    private String userName;

    private int age;

    public String getId()
    {
        return id;
    }

    public void setId(String id)
    {
        this.id = id;
    }

    public String getUserName()
    {
        return userName;
    }

    public void setUserName(String userName)
    {
        this.userName = userName;
    }

    public int getAge()
    {
        return age;
    }

    public void setAge(int age)
    {
        this.age = age;
    }
}

2.5 Repository 定義

Spring Data 為我們提供了很多Repository 接口,我們只需要簡(jiǎn)單的繼承就可以快速實(shí)現(xiàn)領(lǐng)域?qū)ο螅ㄒ簿褪乔懊嫣岬降哪P停┑母鞣NDao層操作。若需要自定義操作,只需要按命名規(guī)范添加接口聲明即可,具體參見官方文檔

這里,我們定義接口UserRepository,繼承JpaRepository<User, String>接口:

package com.mystudy.spring.repository;

import com.mystudy.spring.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String>
{
}

2.6 啟動(dòng)類

這再簡(jiǎn)單不過了:

package com.mystudy.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(UserApplication.class, args);
    }
}

三、Restful 接口設(shè)計(jì)

請(qǐng)注意,本節(jié)是核心內(nèi)容。

3.1 Controller 設(shè)計(jì)

在Spring MVC 中,Restful API的定義對(duì)應(yīng)為Controller層。根據(jù)Restful的接口定義規(guī)范:

GET(SELECT):從服務(wù)器取出資源(一項(xiàng)或多項(xiàng))。
POST(CREATE):在服務(wù)器新建一個(gè)資源。
PUT(UPDATE):在服務(wù)器更新資源(客戶端提供改變后的完整資源)。
PATCH(UPDATE):在服務(wù)器更新資源(客戶端提供改變的屬性)。
DELETE(DELETE):從服務(wù)器刪除資源。

我們?cè)O(shè)計(jì)接口如下所示:

//UserController.java
package com.mystudy.spring.api;

import com.mystudy.spring.domain.User;
import com.mystudy.spring.service.UserService;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/user")
public class UserController
{
    @Autowired
    private UserService userService;

    @ApiOperation(value="獲取用戶列表", notes="獲取用戶列表")
    @GetMapping(value = "/users")
    @ResponseStatus(HttpStatus.OK)
    public List<User> getUserList()
    {
        return userService.getUserList();
    }

    @ApiOperation(value="添加用戶", notes="添加用戶")
    @PostMapping(value = "/users")
    @ResponseStatus(HttpStatus.CREATED)
    public Object addUser(@RequestBody User user){
        return userService.addUser(user);
    }

    @ApiOperation(value="獲取用戶信息", notes="根據(jù)id獲取用戶信息")
    @GetMapping(value = "/users/{id}")
    @ResponseStatus(HttpStatus.OK)
    public Object getUser(@PathVariable("id") String id) throws NotFoundException
    {
        return userService.getUser(id);
    }

    @ApiOperation(value="刪除用戶", notes="根據(jù)id刪除用戶")
    @DeleteMapping(value = "/users/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteUser(@PathVariable("id") String id)
    {
        userService.deleteUser(id);
    }

    @ApiOperation(value="更新用戶", notes="更新用戶")
    @PatchMapping(value = "/users/{id}")
    @ResponseStatus(HttpStatus.CREATED)
    public User updateUser(@PathVariable("id") String id, @RequestBody User user)
    {
        return userService.update(id, user);
    }


    @ApiOperation(value="測(cè)試")
    @GetMapping(value = "/test")
    @ResponseStatus(HttpStatus.OK)
    public String test()
    {
        return "test ok!";
    }
}

如上,各個(gè)接口只是簡(jiǎn)單的將JSON請(qǐng)求進(jìn)行映射,并轉(zhuǎn)發(fā)到對(duì)應(yīng)的Service層,Service層負(fù)責(zé)具體的業(yè)務(wù)處理。

在這些接口上,我們使用了如下注解:

  • @GetMapping
  • @PostMapping
  • @DeleteMapping
  • @PutMapping

它們?cè)赟pring 4.3中引進(jìn),旨在簡(jiǎn)化常用的HTTP方法的映射,并可以更好地表達(dá)被注解方法的語(yǔ)義。如@GetMapping實(shí)際上是一個(gè)組合注解,可以直接代替@RequestMapping(method = RequestMethod.GET),我個(gè)人更推薦這種寫法。

并且,在每個(gè)接口定義上,可以看到注解@ApiOperation,這就是我們前面的提到的swagger的應(yīng)用。如果要為某個(gè)接口生成在線文檔,只要在映射上添加該注解即可。似乎是侵入了代碼,但是這點(diǎn)代價(jià)是值得的。在該注解中,value的值為接口說明,notes可以作為接口的簡(jiǎn)單描述。對(duì)應(yīng)swagger的使用,文章后部分將會(huì)介紹。

同時(shí),我們?cè)诿總€(gè)接口上顯示得使用了注解@ResponseStatus,用來標(biāo)識(shí)接口正常返回時(shí)的HTTP狀態(tài)碼。另外,我們還需要注意每個(gè)接口的返回結(jié)果,除了刪除用戶,其他每個(gè)接口都有返回值。這是因?yàn)镽estful 規(guī)范中提到:

GET /collection:返回資源對(duì)象的列表(數(shù)組)
GET /collection/resource:返回單個(gè)資源對(duì)象
POST /collection:返回新生成的資源對(duì)象
PUT /collection/resource:返回完整的資源對(duì)象
PATCH /collection/resource:返回完整的資源對(duì)象
DELETE /collection/resource:返回一個(gè)空文檔

3.2 Service 設(shè)計(jì)

Service層負(fù)責(zé)具體的業(yè)務(wù)邏輯,其封裝了Dao層的操作,如下:

//UserService.java
package com.mystudy.spring.service;

import com.mystudy.spring.domain.User;
import com.mystudy.spring.repository.UserRepository;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

import static com.mystudy.spring.util.Util.getNullPropertyNames;

@Service
public class UserService
{
    @Autowired
    private UserRepository userRepository;

    public User addUser(User user)
    {
        return userRepository.save(user);
    }

    public List<User> getUserList()
    {
        return userRepository.findAll();
    }

    public User getUser(String id)
    {
        return userRepository.findOne(id);
    }

    public void deleteUser(String id)
    {
        userRepository.delete(id);
    }

    public User update(String id, User user)
    {
        User currentInstance = userRepository.findOne(id);

        //支持部分更新
        String[] nullPropertyNames = getNullPropertyNames(user);
        BeanUtils.copyProperties(user, currentInstance, nullPropertyNames);

        return userRepository.save(currentInstance);
    }
}

可以看到,Service的大部分方法只是簡(jiǎn)單的調(diào)用了Repository的接口。這里,我們需要重點(diǎn)關(guān)注update方法。

根據(jù)Restful的思想,我們知道更新操作可以分為全部更新和部分更新。結(jié)合HTTP語(yǔ)義,可以表示為:

PUT /zoos/ID:更新某個(gè)指定動(dòng)物園的信息(提供該動(dòng)物園的全部信息)
PATCH /zoos/ID:更新某個(gè)指定動(dòng)物園的信息(提供該動(dòng)物園的部分信息)

但實(shí)際上,PATCH語(yǔ)義的應(yīng)用并不廣泛。所以,為了方便,我將兩個(gè)接口合在一起,同時(shí)支持全部和部分更新,HTTP動(dòng)詞使用PUT,僅供參考,大家酌情而定。

在實(shí)現(xiàn)部分更新時(shí),有個(gè)問題需要注意。舉例說明,若我們只需要更新User的age字段,前端提供JSON形如:

{
    "id": "8a8194e5645f53a101645f6048470000",
    "age": 12
  }

其旨在更新age為12,但若請(qǐng)求直接映射到User:

@PutMapping(value = "/users/{id}")
    public User updateUser(@PathVariable("id") String id, @RequestBody User user)
    {
        return userService.update(id, user);
    }

則對(duì)象user的userName屬性會(huì)自動(dòng)映射為null,這樣會(huì)導(dǎo)致數(shù)據(jù)庫(kù)中對(duì)應(yīng)的userName字段被置為空,這是無法接受的。

如何解決?很容易想到,通過id查詢出已存在的User對(duì)象(如A),然后將傳入的User對(duì)象(如B)的非空屬性全部拷貝給A即可。

但是,難不成我們還要以一個(gè)一個(gè)的判斷每個(gè)屬性是否為空?大可不必,我們可以引入Spring提供的BeanUtils.copyProperties方法,該方法可以將一個(gè)對(duì)象的屬性值拷貝給另一個(gè)對(duì)象,并可以忽略指定的屬性。因此,我們只需要獲得所有的空值屬性,然后傳遞給BeanUtils.copyProperties即可。

如何獲取空值屬性?參考stackoverflow中的方案,如下所示:

//Util.java
package com.mystudy.spring.util;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.beans.FeatureDescriptor;
import java.util.stream.Stream;

public class Util
{
    public static String[] getNullPropertyNames(Object source) {
        final BeanWrapper wrappedSource = new BeanWrapperImpl(source);
        return Stream.of(wrappedSource.getPropertyDescriptors())
                .map(FeatureDescriptor::getName)
                .filter(propertyName -> wrappedSource.getPropertyValue(propertyName) == null)
                .toArray(String[]::new);
    }
}

但是,該方法其實(shí)有個(gè)大bug,當(dāng)請(qǐng)求中不填寫age時(shí),User對(duì)象中的age會(huì)被映射為0(這是必定的,因?yàn)榛绢愋湍J(rèn)值為0),但只要我們將基本類型替換為引用類型(默認(rèn)值為null)即可解決該問題,也就是修改age的類型為Integer。當(dāng)然,如果你不想使用模型進(jìn)行映射,也可以使用Map等方式。

綜上,通過這種方法,我們實(shí)現(xiàn)了模型的全部更新和部分更新功能,前端只需要通過一個(gè)接口,傳遞模型的全部字段或部分字段即可。這樣,就可以避免出現(xiàn)類似updateByName,updateByAge, updaeByXX等啰嗦、多余的更新接口。

3.3 Swagger

沒錯(cuò),酷炫的東西到了。除了前面提到的注解@ApiOperation,我們還需要實(shí)現(xiàn)一個(gè)配置類:

//Swagger2.java
package com.mystudy.spring.util;

import com.google.common.collect.Sets;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2 {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .protocols(Sets.newHashSet("http")) //協(xié)議,http或https
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.mystudy.spring.api")) //一定要寫對(duì),會(huì)在這個(gè)路徑下掃描controller定義
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("REST接口定義")
                .version("1.0") 
                .description("用于測(cè)試RESTful API")
                .build();
    }
}

如上只使用了一些最基本的功能,還有很多個(gè)性化的配置大家可以自行發(fā)掘。

前面提到過,本項(xiàng)目的服務(wù)端口是8801,啟動(dòng)項(xiàng)目后,訪問http://127.0.0.1:8801/swagger-ui.html,即可出現(xiàn)在線文檔:

image.png

如上,每個(gè)接口都可以看到詳細(xì)的參數(shù),并可直接進(jìn)行請(qǐng)求測(cè)試。如在添加用戶接口中,我們先點(diǎn)擊按鈕“try out”,接著填寫相關(guān)參數(shù):


image.png

點(diǎn)擊按鈕“Exceute”即可執(zhí)行,結(jié)果顯示為成功:


image.png

綜上,可以發(fā)現(xiàn),結(jié)合Spring Boot時(shí)Swagger比Postman使用更加便捷。當(dāng)然,這里只介紹了Swagger最基本的應(yīng)用,更多特性請(qǐng)谷歌之。

四、Restful 錯(cuò)誤響應(yīng)

另外一個(gè)重點(diǎn)來了,也是一個(gè)難點(diǎn)。目前,我們只介紹了Restful 最簡(jiǎn)單的正常使用場(chǎng)景,沒有介紹Restful 的錯(cuò)誤響應(yīng)的處理方式。在實(shí)際的前后端分離的開發(fā)中,服務(wù)端的多種錯(cuò)誤情況都必須要反饋給前端進(jìn)行處理。

4.1 錯(cuò)誤響應(yīng)風(fēng)格

對(duì)于錯(cuò)誤響應(yīng),結(jié)合阮一峰的文章,我個(gè)人傾向的風(fēng)格是:

  1. 正常的響應(yīng)應(yīng)該直接返回需要的數(shù)據(jù),而無需嵌套或添加任何額外信息。此時(shí)HTTP的返回碼可以為:
200 OK - [GET]:服務(wù)器成功返回用戶請(qǐng)求的數(shù)據(jù),該操作是冪等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數(shù)據(jù)成功。
202 Accepted - [*]:表示一個(gè)請(qǐng)求已經(jīng)進(jìn)入后臺(tái)排隊(duì)(異步任務(wù))
204 NO CONTENT - [DELETE]:用戶刪除數(shù)據(jù)成功。
  1. 如果狀態(tài)碼是4xx,就應(yīng)該向用戶返回出錯(cuò)信息。一般來說,返回的信息中將error作為鍵名,出錯(cuò)信息作為鍵值即可:
{
      error: "error message"
}

此時(shí),HTTP的狀態(tài)碼就是相應(yīng)的業(yè)務(wù)錯(cuò)誤碼,一般無需額外定義業(yè)務(wù)錯(cuò)誤碼。但很多時(shí)候,僅有的20多個(gè)HTTP狀態(tài)碼不足以表達(dá)服務(wù)端的所有異常,此時(shí),我們就需要額外定義錯(cuò)誤碼,而HTTP狀態(tài)碼僅表示錯(cuò)誤的大類型。例如,若查找的指定用戶信息不存在,HTTP狀態(tài)碼依舊為404,響應(yīng)可以返回:

{
  "code": 40401,
  "error": "user 11 not found!"
}

其中,code為自定義錯(cuò)誤碼40401,error為其對(duì)應(yīng)的錯(cuò)誤內(nèi)容。40401的前綴404表示資源不存在,01可以表示具體表示user這種資源不存在。

當(dāng)然,Restful 的錯(cuò)誤響應(yīng)風(fēng)格并不局限于此,大家可以根據(jù)實(shí)際情況和使用習(xí)慣酌情考慮,我唯一建議的就是——合理利用HTTP錯(cuò)誤碼而不是完全棄之不顧。

4.2 Spring 統(tǒng)一異常處理

介紹完錯(cuò)誤響應(yīng)風(fēng)格后,我們考慮如何在Spring Boot中實(shí)現(xiàn)之。很多人的的做法是將各種錯(cuò)誤轉(zhuǎn)化為錯(cuò)誤響應(yīng)對(duì)象進(jìn)行返回。首先,我們定義一個(gè)表示錯(cuò)誤響應(yīng)的對(duì)象:

public class Result
{
    /**
     * 錯(cuò)誤內(nèi)容
     */
    private String error;

    /**
     * 自定義錯(cuò)誤碼
     */
    private int code;


    public Result(String error, int code)
    {
        this.error = error;
        this.code = code;
    }

    public String getError()
    {
        return error;
    }

    public void setError(String error)
    {
        this.error = error;
    }

    public int getCode()
    {
        return code;
    }

    public void setCode(int code)
    {
        this.code = code;
    }


    public enum ErrorCode{
        /**
         * 用戶不存在
         */
        USER_NOT_FOUND(40401),

        /**
         * 用戶已存在
         */
        USER_ALREADY_EXIST(40001),
        ;

        private int code;

        public int getCode()
        {
            return code;
        }

        ErrorCode(int code)
        {
            this.code = code;
        }
    }
}

可以看到,該對(duì)象中還定義了錯(cuò)誤碼的枚舉類。接著,我們修改返回接口的返回類型為Object:

    @GetMapping(value = "/users/{id}")
    public Object getUser(@PathVariable("id") String id)
    {
        return userService.getUser(id);
    }

我們考慮被添加的用戶已存在的錯(cuò)誤情況,修改Service:

    public Object getUser(String id)
    {
        User currentInstance = userRepository.findOne(id);
        if (currentInstance == null)
        {
            return new Result("user " + id + "is exist!",
                    Result.ErrorCode.USER_ALREADY_EXIST.getCode());
        }
        return userRepository.findOne(id);
    }

好了,我們現(xiàn)在使用Swagger進(jìn)行測(cè)試,查找一個(gè)不存在的用戶abc

image.png

返回結(jié)果如我們所料,但是HTTP的響應(yīng)碼卻還是200,應(yīng)該是404。所以,緊靠這些無法滿足我們的需求。當(dāng)然,我們可以自定義攔截器實(shí)現(xiàn)響應(yīng)碼修改。這里,有一個(gè)更好的解決方案——Spring 全局異常處理機(jī)制。我們可以通過使用@ControllerAdvice注解定義全局統(tǒng)一的異常處理類來完成需求。

也即是說,在處理錯(cuò)誤時(shí),我們不再直接返回Result對(duì)象,而采用異常機(jī)制。其實(shí),我個(gè)人也覺得代碼中到處返回Result對(duì)象真是一個(gè)bad smell。在Java中,錯(cuò)誤得情況難道還有比異常更好的表現(xiàn)方式么?

好吧,廢話太多了,開始實(shí)現(xiàn)。新建一個(gè)全局異常GlobalException ,其作為眾多自定義異常的父類:

public class GlobalException extends Exception {

    private int code;

    public GlobalException(String message)
    {
        super(message);
    }

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

    public void setCode(int code)
    {
        this.code = code;
    }

    public int getCode()
    {
        return code;
    }
}

新建一個(gè)自定義異常NotFoundException,該異常專門用來表示各種類型資源不存在的異常情況:

public class NotFoundException extends GlobalException
{
    public NotFoundException(String message, int code)
    {
        super(message, code);
    }
}

新建類RestExceptionHandler,使用注解@ControllerAdvice,如下:

@ControllerAdvice
public class RestExceptionHandler
{
    private static Logger logger = LoggerFactory.getLogger(RestExceptionHandler.class);

    @ExceptionHandler(value = NotFoundException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Result handleResourceNotFoundException(NotFoundException e)
    {
        logger.error(e.getMessage(), e);
        return new Result(e.getMessage(), e.getCode());
    }
}

如上,通過使用注解@ControllerAdvice,類RestExceptionHandler就可以實(shí)現(xiàn)全局異常的攔截處理功能。自定義的方法handleResourceNotFoundException旨在攔截NotFoundException異常,一旦攔截成功后,我們可以進(jìn)行各種處理操作,并且返回自己想要的結(jié)果。

其中,注解@ExceptionHandler表示要攔截的異常;注解@ResponseStatus可以指定HTTP響應(yīng)的狀態(tài)碼;當(dāng)然,注解@ResponseBody也必不可少。

OK,讓我們先修改之前的用戶查找接口,并且拋出異常:

    public Object getUser(String id) throws NotFoundException
    {
        User currentInstance = userRepository.findOne(id);
        if (currentInstance == null)
        {
            throw new NotFoundException("user " + id + " is not exist!", Result.ErrorCode.USER_NOT_FOUND.getCode());
        }
        return userRepository.findOne(id);
    }

當(dāng)然,Controller也要拋出異常:

    @GetMapping(value = "/users/{id}")
    public Object getUser(@PathVariable("id") String id) throws NotFoundException
    {
        return userService.getUser(id);
    }

OK,重新請(qǐng)求一個(gè)不存在的用戶嘗試一下:


image.png

如上,如我們所愿,HTTP響應(yīng)碼也返回了,同時(shí)查看服務(wù)控制臺(tái):

com.mystudy.spring.exception.NotFoundException: user abc is not exist!
    at com.mystudy.spring.service.UserService.getUser(UserService.java:36) ~[classes/:na]
    at com.mystudy.spring.api.UserController.getUser(UserController.java:36) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:133) ~[spring-web-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:901) ~[spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) [spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) [spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:635) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) [spring-webmvc-4.3.18.RELEASE.jar:4.3.18.RELEASE]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.31.jar:8.5.31]
省略

沒錯(cuò),這才打開的正確方式!一旦接口拋出異常,Spring 馬上攔截并進(jìn)行處理,最后返回自定義的錯(cuò)誤對(duì)象。當(dāng)然,若接口一切正常,還是按正常邏輯返回模型對(duì)象。

同理,我們還可以新建多種其他異常,比如表示非法參數(shù)、權(quán)限不足等。需要注意的是,請(qǐng)不要為每種資源都新建異常,比如你不需要?jiǎng)?chuàng)建UserNotFoundExceptionBookNotFoundException等,否則會(huì)顯得多么繁瑣。

五、后語(yǔ)

至此,本文的目標(biāo)已經(jīng)達(dá)成,首先介紹了如何使用Spring Boot設(shè)計(jì)Restful API,然后介紹了常用的Restful 錯(cuò)誤響應(yīng)風(fēng)格,最后利用Spring Boot的全局異常處理機(jī)制實(shí)現(xiàn)了Restful 的錯(cuò)誤響應(yīng)功能。

項(xiàng)目源碼請(qǐng)戳:https://gitee.com/haoranjunzi/study-restful

本人水平有限,難免有錯(cuò)誤或遺漏之處,望大家指正和諒解,歡迎評(píng)論留言。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,481評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,697評(píng)論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,182評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,406評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,933評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,772評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,973評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,209評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評(píng)論 1 285
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,644評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,953評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容