什么是異常?
通俗的說就是,讓你感覺不爽的,阻礙你的事都算異常,也就是說不讓我們程序正常運行的情況。
為什么要統一處理異常?
方便集中管理,集中定位問題
異常實例
舉個例子,還用之前的學生信息那個案例,我們添加一個小於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,有興趣的同學自己可以去嘗試。
學習他人的優點,對比自己的不足!