spring boot 中統一異常處理


什么是異常?

通俗的說就是,讓你感覺不爽的,阻礙你的事都算異常,也就是說不讓我們程序正常運行的情況。

為什么要統一處理異常?

方便集中管理,集中定位問題

異常實例

舉個例子,還用之前的學生信息那個案例,我們添加一個小於18歲的學生,調用接口,控制台報錯如下:

 

 

 再看接口返回信息,如下圖:

     

 

 

 

 

 添加失敗                                                                                                添加成功

 

 

暫且先不說控制台報錯,對比下,我們添加成功的接口信息返回情況,明顯這給客戶端調用我們程序的同學,有些不便,那么我們這里做下優化。

1、統一格式化輸出json

 強迫症的我,這里有必要做下統一格式的輸出,那么具體怎么做呢?

增加一個外層對象,用於包裹里面對象,具體代碼示例如下:

package com.rongrong.springboot.demo.domain;

import lombok.Data;

/**
 * @description: 最外層對象
 * @author rongrong
 * @version 1.0
 * @date 2020/1/9 21:51
 */
@Data
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;
}

針對成功、失敗,定制統一的工具類,具體示例代碼如下:

package com.rongrong.springboot.demo.utils;

import com.rongrong.springboot.demo.domain.Result;

/**
 * @description: 統一格式化輸出json
 * @author rongrong
 * @version 1.0
 * @date 2020/1/9 21:55
 */
public class ResultUtils {

    public static Result success(Object obj){
        Result result = new Result();
        result.setCode(0);
        result.setMsg("success");
        result.setData(obj);
        return result;
    }

    public static Result success(){
        return success(null);
    }

    public static Result error(String msg){
        Result result = new Result();
        result.setCode(-1);
        result.setMsg(msg);
        //result.setMsg("unknown error");
        return result;
    }
}

接着我們需要對添加學生的接口進行改造,將我們封裝好的工具類引入,達到統一輸出的效果,具體代碼如下:

 /**
     * 新增一個學生
     *
     * @return
     */
    @PostMapping("/studentAdd")
    public Result<Student> sudentAdd(@Valid Student student, BindingResult bindingResult) {
        if(bindingResult.hasFieldErrors()){
            Result result = ResultUtils.error(bindingResult.getFieldError().getDefaultMessage());
            //輸出錯誤信息
            //System.out.println(bindingResult.getFieldError().getDefaultMessage());
            return result;
        }
        student.setName(student.getName());
        student.setAge(student.getAge());
        student.setSex(student.getSex());
        student.setEmail(student.getEmail());
        Result result = ResultUtils.success(studentResponstory.save(student));
        //保存和更新都用該方法
        return result;
    }

我們調用接口服務,再來看接口返回,如下圖:

     

 

 

 再來看下,明顯舒服好多了。

2、多個異常情況的統一

現在我們實現這樣一組功能,獲取學生的年齡並判斷,小於10歲,返回“應該上小學”,大於10歲且小於16歲,返回“應該上初中了”

我們需要在StudentService中寫邏輯,供controller調用,具體代碼如下:

   /**
     * 查詢學生年齡
     *
     * @param id
     * @throws Exception
     */
    public void getStudnetAge(Integer id) throws Exception {
        Student student = studentResponstory.findOne(id);
        Integer age = student.getAge();
        //小於10歲,返回“應該上小學”,大於10歲且小於16歲,返回“應該上初中了”
        if (age <= 10) {
            throw new Exception("應該上小學");
        } else if (age > 10 && age < 16) {
            throw new Exception("應該上小學");
        }
    }

接着我們在StudentController中調用,具體代碼示例如下:

   /**
     * 獲取學生年齡
     * @param id
     * @throws Exception
     */
    @GetMapping("/students/getAge/{id}")
    public void getAge(@PathVariable("id") Integer id) throws Exception {
        studentService.getStudnetAge(id);
    }

數據庫中學生的信息如下:

我們先來查詢id為13、15、16的學生,查看接口返回信息如下:

         

 異常不一樣,我們需要再次進行統一化管理了,輸出統一格式化后的json。

3、使用 @ControllerAdvice 實現全局異常處理

顯然我們需要把message中的信息及code組合外部對象,在包裝內部返回data對象,這時需要我們使用 @ControllerAdvice 進行全局異常處理,配合@ExceptionHandle注解使用,@ExceptionHandle注解可以自動捕獲controller層出現的指定類型異常,並對該異常進行相應的異常處理。

我們先來建立一個統一的異常類,繼承RuntimeException,因為對於spring boot框架中,只有RuntimeException類的異常才會進行事務回滾,具體示例代碼如下:

package com.rongrong.springboot.demo.exception;

/**
 * @author rongrong
 * @version 1.0
 * @description:
 * @date 2020/1/10 0:24
 */
public class StudentException extends RuntimeException{
    //code碼
    private Integer code;
    //錯誤信息
    private String msg;

    public StudentException(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

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

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

注意:此處必須用getSet方法,不能lombok插件,否則會報錯,沒有定義getSet方法。

接着我們再來編寫全局異常處理,並針對異常類型做出判斷,具體示例代碼如下:

package com.rongrong.springboot.demo.handle;

import com.rongrong.springboot.demo.domain.Result;
import com.rongrong.springboot.demo.exception.StudentException;
import com.rongrong.springboot.demo.utils.ResultUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @description: 全局異常處理
 * @author rongrong
 * @version 1.0
 * @date 2020/1/10 0:17
 */
@ControllerAdvice
public class ExceptionHandle {

    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Result error(Exception e) {
        if(e instanceof StudentException){
            StudentException studentException=(StudentException)e;
            return ResultUtils.error(studentException.getCode(),studentException.getMsg());
        }else {
            return ResultUtils.error(-1, "unknown error!");
        }
    }
}

同樣的,我們需要對StudentService中作出調整,修改為我們自定義的異常,具體示例代碼如下:

  /**
     * 查詢學生年齡
     *
     * @param id
     * @throws Exception
     */
    public void getStudnetAge(Integer id) throws Exception {
        Student student = studentResponstory.findOne(id);
        Integer age = student.getAge();
        //小於10歲,返回“應該上小學”,大於10歲且小於16歲,返回“應該上初中了”
        if (age <= 10) {
            throw new StudentException(100,"應該上小學");
        } else if (age > 10 && age < 16) {
            throw new StudentException(101,"應該上初中了");
        }
    }

重新啟動項目,再次調用查詢學生年齡接口,查看返回結果如下所示證明成功。

   

 

 

 4、對code碼的統一管理維護

很明顯,現在兩個報錯對應兩個code和msg,那么如果有多種code和msg對應的話這里感覺維護起來就很難了,所以我們要把它拿出來統一集中管理就好,這里使用枚舉,來實現code和msg的映射。

具體示例代碼如下:

package com.rongrong.springboot.demo.exceptionenum;

/**
 * @author rongrong
 * @version 1.0
 * @description:
 * @date 2020/1/9 23:11
 */

public enum ResultEnum {

    UNKNOW_ERROR(-1,"unknown error!"),
    HIGH_SCHOOL(10001,"應該上小學啦!"),
    PRIMARY_SCHOOL(10002,"應該上初中啦!"),
    SUCCESS(0,"success");
    //code碼
    private Integer code;
    //錯誤信息
    private String msg;

    public Integer getCode() {
        return code;
    }

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

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

接下來,需要我們在對StudentService中作出調整,修改為我們自定義的異常,傳參為我們的枚舉對象,具體示例代碼如下:

/**
     * 查詢學生年齡
     *
     * @param id
     * @throws Exception
     */
    public void getStudnetAge(Integer id) throws Exception {
        Student student = studentResponstory.findOne(id);
        Integer age = student.getAge();
        //小於10歲,返回“應該上小學”,大於10歲且小於16歲,返回“應該上初中了”,其他正常輸出
        if (age <= 10) {
            throw new StudentException(ResultEnum.PRIMARY_SCHOOL);
        } else if (age > 10 && age < 16) {
            throw new StudentException(ResultEnum.HIGH_SCHOOL);
        }else {
            throw new StudentException(ResultEnum.SUCCESS);
        }
    }

接着在對,StudentException這個異常構造器,做下調整,具體代碼如下:

package com.rongrong.springboot.demo.exception;

import com.rongrong.springboot.demo.exceptionenum.ResultEnum;

/**
 * @author rongrong
 * @version 1.0
 * @description:
 * @date 2020/1/10 0:24
 */
public class StudentException extends RuntimeException{
    //code碼
    private Integer code;
    //錯誤信息
    private String msg;

    public StudentException(ResultEnum resultEnum) {
        this.code = resultEnum.getCode();
        this.msg = resultEnum.getMsg();
    }

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

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

最后,我們再來啟動項目,調用下接口,返回如下信息,證明修改成功!

   

 5、單元測試

為了程序能夠更好的運行,我們必須要做測試,所以要養成寫完程序進行單元測試的好習慣。

那么在這里我們需要對Service、API進行測試。

5.1、對service進行單元測試

可以通過自定義創建類,來編寫單元測試,也可以通過idea向導來創建,具體操作如下圖所示:

具體示例代碼如下:

package com.rongrong.springboot.demo.controller;

import com.rongrong.springboot.demo.domain.Student;
import com.rongrong.springboot.demo.responstory.StudentResponstory;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @description: 對service進行單元測試
 * @author rongrong
 * @version 1.0
 * @date 2020/1/10 20:52
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentControllerTest {

    @Autowired
    StudentResponstory studentResponstory;

    @Test
    public void sudentFindOne() {
        Student student = studentResponstory.findOne(13);
        Assert.assertEquals(new Integer(25), student.getAge());
    }
}
5.2、對API進行測試

使用@AutoConfigureMockMvc注解,配合MockMvcRequestBuilders、MockMvcResultMatchers來測試,具體示例代碼如下:

package com.rongrong.springboot.demo.controller;

import com.rongrong.springboot.demo.responstory.StudentResponstory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

/**
 * @description: 對API進行單元測試
 * @author rongrong
 * @version 1.0
 * @date 2020/1/10 21:12
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class StudentApiTest {


    @Autowired
    MockMvc mockMvc;

    @Test
    public void testStudentApiTest() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/students"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("student"));
    }

}

運行測試,結果如下:

 

到此,spring boot 中統一異常處理,AutoConfigureMockMvc這個注解,感覺與powermock很像,其中各種APi,有興趣的同學自己可以去嘗試。

 

學習他人的優點,對比自己的不足!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM