前言
這篇文章的將介紹表單驗證,AOP處理請求和統一異常處理,案例是延續上一篇 SpringBoot初識
表單驗證
現在將要攔截未滿18歲的女生,在之前GirlController里面添加一個女生的方法如下:
方法的形參使用的都是屬性,那以后當屬性變多的時候再來管理就會變得很復雜,直接傳遞Girl對象就是最好的方法。
現在要對年齡做限制,先進入Girl實體為age屬性添加 @Min注解
接着在添加女生的方法上添加 @Valid注解,表示要驗證這個對象。而驗證完之后要知道是驗證通過還是沒通過,它會將驗證的結果返回到BindingResult對象里,如果有錯誤,要將它打印出來。
@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);
}
此時傳入一個年齡合法的女生:
再傳入一個年齡小於18歲的女生:
控制台報錯並打印錯誤信息:
數據庫中也沒有添加剛才的信息:
AOP處理請求
AOP是一種編程范式,與語言無關,它是一種程序設計思想。面向對象關注的是將需求功能垂直划分為不同的並且相對獨立的,它會封裝為良好的類,並且有屬於自己的行為。而AOP則是利用橫切的技術,將面向對象構建的龐大類的體系進行水平的切割,並且會將影響到了多個類的公共行為封裝為一個可重用的模塊,這個模塊就稱為切面。AOP的關鍵思想就是將通用邏輯從業務邏輯中分離出來。
添加pom依賴
<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好了很多。
打印Http請求
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 ");
}
}
執行查詢后,控制台打印:
AfterReturning注解
使用這個注解可以得到執行方法之后的返回信息,也就是
添加注解:
再次執行查詢,控制台打印:
可以看到這里的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類
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;
}
}
修改Controller中girlAdd方法
添加一條沒有金額的女生:
添加一條有金額但是未滿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繼續拋出,這樣當條件滿足時,這個異常信息(也就是那個字符串)就會在控制台上出現。
Controller中新添加一個方法
實現邏輯通過service來處理
不過這樣還是沒有達到本來的目的,我們的目的是,瀏覽器返回的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,現在控制台就可以找到該系統異常問題所在:
使用枚舉封裝code和message
在前面所拋出的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;
}
}
修改Service中的方法
GirlException中的構造方法也要修改:
ResultUtil中的無參success方法
在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,扎心了。
單元測試
測試Service
在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測試完畢。
測試API
選擇對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