這篇文章的將介紹表單驗證,AOP處理請求和統一異常處理,案例是延續上一篇 SpringBoot初識html
如今將要攔截未滿18歲的女生,在以前GirlController裏面添加一個女生的方法以下:java
方法的形參使用的都是屬性,那之後當屬性變多的時候再來管理就會變得很複雜,直接傳遞Girl對象就是最好的方法。git
如今要對年齡作限制,先進入Girl實體爲age屬性添加 @Min註解github
接着在添加女生的方法上添加 @Valid註解,表示要驗證這個對象。而驗證完以後要知道是驗證經過仍是沒經過,它會將驗證的結果返回到BindingResult對象裏,若是有錯誤,要將它打印出來。web
@PostMapping("/girls") public Girl girlAdd(@Valid Girl girl, BindingResult bindingResult) { if (bindingResult.hasErrors()) { System.out.println(bindingResult.getFieldError().getDefaultMessage()); return null; } return girlRepository.save(girl); }
此時傳入一個年齡合法的女生:spring
再傳入一個年齡小於18歲的女生:數據庫
控制檯報錯並打印錯誤信息:編程
數據庫中也沒有添加剛纔的信息:json
AOP是一種編程範式,與語言無關,它是一種程序設計思想。面向對象關注的是將需求功能垂直劃分爲不一樣的而且相對獨立的,它會封裝爲良好的類,而且有屬於本身的行爲。而AOP則是利用橫切的技術,將面向對象構建的龐大類的體系進行水平的切割,而且會將影響到了多個類的公共行爲封裝爲一個可重用的模塊,這個模塊就稱爲切面。AOP的關鍵思想就是將通用邏輯從業務邏輯中分離出來。api
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
在新建的aspect包裏新建HttpAspect類,設置切點攔截GirlController類裏面的全部方法,而後把 @Pointcut註解放在一個空的方法上log(),以後的前置加強和後置加強就直接使用 @Before("log()")註解做用在方法上便可。
爲了優雅的打印結果,就不在使用system.out了,使用日誌打印結果.
package com.zzh.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; @Aspect @Component public class HttpAspect { private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class); @Pointcut("execution(public * com.zzh.controller.GirlController.*(..))") public void log() { } @Before("log()") public void doBefore() { logger.info("This is Before"); } @After("log()") public void doAfter() { logger.info("This is After "); } }
接着在Controller的查詢方法裏面添加一行日誌打印來觀察日誌輸出順序
查看打印結果:
採用記錄日誌的方式,會更爲詳細的打印出該條語句相關的信息,比System.out好了不少。
package com.zzh.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; @Aspect @Component public class HttpAspect { private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class); @Pointcut("execution(public * com.zzh.controller.GirlController.*(..))") public void log() { } @Before("log()") //記錄Http請求 public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); //url logger.info("url={}",request.getRequestURL()); //method logger.info("method={}",request.getMethod()); //ip logger.info("ip={}",request.getRemoteAddr()); //類方法 logger.info("class_method={}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); //參數 logger.info("args={}",joinPoint.getArgs()); } @After("log()") public void doAfter() { logger.info("This is After "); } }
執行查詢後,控制檯打印:
使用這個註解能夠獲得執行方法以後的返回信息,也就是
添加註解:
再次執行查詢,控制檯打印:
能夠看到這裏的response打印出了對象,可是具體的信息沒有打印出來,此時須要在實體Girl裏面重寫toString方法便可。
從新執行查詢,能夠看到具體信息打印出來了:
在實體Girl中增長money字段,同時在money屬性上增長 @NotNull註解,也就是當咱們不傳入money時會報錯。
不傳入money信息:
控制檯報錯:
這裏出現了空指針異常,它是HttpAspect類中doAfterReturning拋出的,這是由於在Controller的girlAdd方法裏增長了表單驗證,返回了null,而到了doAfterReturning方法時,還調用了object.toString方法因此拋出了異常。
當沒有傳入金額時,「金額必傳」是由控制檯打印輸出,而若是改成在網頁上輸出,改變Controller中的girlAdd方法,將錯誤信息直接return給網頁,注意返回類型須要改成Object,由於成功的時候是返回Girl對象。
繼續添加一個沒有傳入金額的女生,網頁返回字符串:
控制檯打印「字符串」,由於如今的對象就是這個錯誤信息:
上面介紹了若是出現錯誤返回字符串,若是正確就返回json,這樣格式很混亂,因此須要進行整理。
好比若是金額不符合,就返回{"code":1, "msg":"金額必傳", "data":null}。成功的話就是{"code":0, "msg":"成功", "data":{"id":20,"cupSize":"B","age":25,"money":1.2}}這樣的格式。
Result類做爲http請求返回的最外層對象
package com.zzh.domain; public class Result<T> { //錯誤碼 private Integer code; //提示信息 private String msg; //具體內容 private T data; 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; } public T getData() { return data; } public void setData(T data) { this.data = data; } }
添加一條沒有金額的女生:
添加一條有金額可是未滿18歲的女生:
添加一條信息正確的女生:
能夠看到上面的代碼的result的相關操做已經重複調用了,因此新建立ResultUtil類來封裝重複操做。
package com.zzh.utils; import com.zzh.domain.Result; public class ResultUtil { public static Result success(Object object) { Result result = new Result(); result.setCode(0); result.setMsg("成功"); result.setData(object); return result; } public static Result success() { return success(null); } public static Result error(Integer code, String msg) { Result result = new Result(); result.setCode(code); result.setMsg(msg); return result; } }
此時Controller中的girlAdd方法簡化以下:
測試獲得的結果跟以前的同樣,可是Controller中的重複代碼省略了。
如今須要獲取女生的年齡並判斷,若是小於10,就返回一個字符串,若是大於10小於16又返回另一個字符串。首先想到的就是直接在Service中寫一個判斷邏輯,返回類型設爲String,符合條件的直接return那個字符串就行,這樣作也能夠,可是若是判斷完以後我還要作一些其餘的事情,那麼這個返回類型就已經限制了功能的擴展。
這時用異常來處理就很好,知足條件,直接throw給上一層,也就是Controller,而後Controller繼續拋出,這樣當條件知足時,這個異常信息(也就是那個字符串)就會在控制檯上出現。
不過這樣仍是沒有達到原本的目的,咱們的目的是,瀏覽器返回的Json要是以前設置好的code,msg,data,而後msg字段就用來顯示拋出的字符串。
解決的方法就是對Controller拋出的內容進行捕獲,取到須要的內容封裝起來再返回給瀏覽器。
這裏是對Controller進行異常捕獲,須要加上 @ControllerAdvice註解
package com.zzh.handle; import com.zzh.domain.Result; import com.zzh.utils.ResultUtil; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice public class ExceptionHandler { @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class) @ResponseBody public Result handle(Exception e) { return ResultUtil.error(100, e.getMessage()); } }
此時在數據庫中設置一條記錄:
經過方法測試第三條數據:
如今的異常信息返回的code都是100,若是要劃分異常,好比年齡小於10的code設爲100,而大於10小於16的code設爲101,劃分以後更方便排查問題。而Exception裏面只能傳message,不能再傳code進去了,因此須要本身定義異常。
自定義異常沒有繼承Exception,而是繼承RuntimeException是有緣由的,RuntimeException是繼承Exception,可是Spring只對RuntimeException進行事務回滾,若是拋出的是Exception是不會回滾的。
package com.zzh.exception; public class GirlException extends RuntimeException{ private Integer code; public GirlException(Integer code,String message) { super(message); this.code = code; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } }
Service中的方法也須要修改,將拋出的異常改成自定義的異常:
在以前設定的ExceptionHandler捕獲的是Exception,因此須要進行判斷異常是否是本身定義的異常。若是不是就把code設置爲-1,message設置爲未知錯誤。
package com.zzh.handle; import com.zzh.domain.Result; import com.zzh.exception.GirlException; import com.zzh.utils.ResultUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice public class ExceptionHandler { private final static Logger logger = LoggerFactory.getLogger(ExceptionHandler.class); @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class) @ResponseBody public Result handle(Exception e) { //判斷異常是否是本身定義的異常 if (e instanceof GirlException) { GirlException girlException = (GirlException) e; return ResultUtil.error(girlException.getCode(), girlException.getMessage()); } else { logger.error("[系統異常] {}", e); return ResultUtil.error(-1, "未知錯誤"); } } }
測試:
要測試自定義異常裏的系統異常要怎麼樣作呢?好比經過不傳入金額讓它報系統異常,稍微改動一點就能夠了:
爲何要改成return null呢,若是不改的話code就會是1了,只有改成了null,切面裏的object.toString纔會報錯。
不傳入金額:
以前在ExceptionHandler設置了Logger,如今控制檯就能夠找到該系統異常問題所在:
在前面所拋出的GirlException中,是直接將code和message做爲參數進行傳遞,這樣很不容易作後期維護,若是code和message統一封裝起來就很方便進行維護了。
枚舉裏面只須要有屬性的Getter方法便可,由於枚舉的使用都是經過構造方法來建立,不會再使用Setter。
package com.zzh.enums; public enum ResultEnum { UNKONW_ERROR(-1, "未知錯誤"), SUCCESS(0, "成功"), PRIMARY_SCHOOL(100, "你可能還在上小學"), MIDDLE_SCHOOL(101, "你可能還在上初中"),; private Integer code; private String msg; ResultEnum(Integer code, String msg) { this.code = code; this.msg = msg; } public Integer getCode() { return code; } public String getMsg() { return msg; } }
GirlException中的構造方法也要修改:
在ResultUtil中總共定義了3個方法,一個是有參的success方法,當添加女生信息正確的時候須要將Girl對象做爲參數傳給success方法,再由ResultUtil進行封裝後傳給瀏覽器。
而ResultUtil中的error方法也相似,反正就是將code和錯誤信息進行封裝。
那這裏的無參success方法是用在什麼地方呢,我先執行一下Controller中刪除單個女生的方法:
數據正常刪除,不過返回信息和控制檯信息卻不是很友好:
緣由顯而易見了,設置的切面AfterReturning中有object.toString方法,我Controller中這個刪除的方法沒有返回值(void)。天然就報了空指針異常,而後這個異常被ExceptionHandler捕獲,設置了code和msg值,以此傳遞給瀏覽器。
修改的方法就是使用無參的success方法:
設置了Result做爲返回值,切面就不會報錯,同時無參success方法體裏再調用有參的success方法,只不過object爲null,這樣一來就很友好的顯示了。
執行方法:
完美刪除!
P.S 說個笑話,剛纔在使用RESTClient進行刪除操做時,Ctrl+Enter是執行的快捷鍵,也就是能夠替代點擊綠色的按鈕。我先按下了Ctrl,而後再按下了Enter,報錯!!可是數據正常刪除,仔細查看控制檯輸出錯誤信息,上面顯示我執行了兩次刪除操做,對同一個id進行兩次刪除想一想都知道確定會報錯,可是我只按了一次快捷鍵呀,而後我嘗試不用快捷鍵而是去點擊綠色執行按鈕,不管是控制檯仍是瀏覽器返回都TM正常!帶着疑惑吃了飯回來,腦洞大開同時按下Ctrl+Enter,一切問題解決,都不須要Google,扎心了。
在GirlService中新建要測試的方法:
接着按下Ctrl+Shift+T,快速建立一個測試類,勾選要測試的方法:
在測試類中使用斷言,將指定id女生的年齡提取出來與設置進行比較。
package com.zzh.service; import com.zzh.domain.Girl; 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; @RunWith(SpringRunner.class) @SpringBootTest public class GirlServiceTest { @Autowired private GirlService girlService; @Test public void findOne() throws Exception { Girl girl = girlService.findOne(2); Assert.assertEquals(new Integer(25), girl.getAge()); } }
測試結果:
如今將設置的年齡改成17,也就是: Assert.assertEquals(new Integer(17), girl.getAge());
測試很友好的告訴了咱們,這個ID對應的真實年齡是25,可是咱們期待的是17。Service測試完畢。
選擇對Controller中girlList方法進行測試:
這裏使用的不是girlController對象調用girlList方法,這樣一來跟URL徹底沒有關係了,這裏的測試須要像以前使用的RESTClient,給一個地址,而後發出Get請求,獲得結果,這樣纔是API測試。
這就須要使用MockMvc這個類了,注意添加 @AutoConfigureMockMvc註解:
package com.zzh.controller; 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; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class GirlControllerTest { @Autowired private MockMvc mvc; @Test public void girlList() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/girls")) .andExpect(MockMvcResultMatchers.status().isOk()); } }
這樣作就會對這個請求地址的狀態碼進行判斷:
如今將請求地址故意改錯:("/girls234")
能夠看到咱們期待的狀態是200,可是實際爲404.
除了狀態以外還能夠作其餘判斷,好比對返回的內容進行判斷,期待的是abc,但實際是一個json字符串:
測試:
對API的測試和對Service的測試區別在於要使用MockMvc進行測試。
本文簡單介紹瞭如何使用 @Valid表單驗證,而後是使用AOP處理請求,接着是統一異常處理,最後是對Service和API的單元測試。
Github地址:
SpringBoot-girl