使用驗證注解來實現(xiàn)表單驗證
雖說前端的h5和js都可以完成表單的字段驗證,但是這只能是防止一些小白铸豁、誤操作而已。如果是一些別有用心的人菊碟,是很容易越過這些前端驗證的节芥,有句話就是說永遠(yuǎn)不要相信客戶端傳遞過來的數(shù)據(jù)。所以前端驗證之后逆害,后端也需要再次進行表單字段的驗證头镊,以確保數(shù)據(jù)到后端后是正確的、符合規(guī)范的魄幕。本節(jié)就簡單介紹一下相艇,在SpringBoot的時候如何進行表單驗證。
首先創(chuàng)建一個SpringBoot工程梅垄,其中pom.xml配置文件主要配置內(nèi)容如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
創(chuàng)建一個pojo類厂捞,在該類中需要驗證的字段上加上驗證注解输玷。代碼如下:
package org.zero01.domain;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
public class Student {
@NotNull(message = "學(xué)生名字不能為空")
private String sname;
@Min(value = 18,message = "未成年禁止注冊")
private int age;
@NotNull(message = "性別不能為空")
private String sex;
@NotNull(message = "聯(lián)系地址不能為空")
private String address;
public String toString() {
return "Student{" +
"sname='" + sname + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
... getter setter 略 ...
}
創(chuàng)建一個Controller類:
package org.zero01.controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zero01.domain.Student;
import javax.validation.Valid;
@RestController
public class StudentController {
@PostMapping("register.do")
public Student register(@Valid Student student, BindingResult bindingResult){
if (bindingResult.hasErrors()) {
// 打印錯誤信息
System.out.println(bindingResult.getFieldError().getDefaultMessage());
return null;
}
return student;
}
}
啟動運行類,代碼如下:
package org.zero01;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SbWebApplication {
public static void main(String[] args) {
SpringApplication.run(SbWebApplication.class, args);
}
}
使用postman進行測試靡馁,年齡不滿18歲的情況:
控制臺打印結(jié)果:
未成年禁止注冊
非空字段為空的情況:
控制臺打印結(jié)果:
學(xué)生名字不能為空
使用AOP記錄請求日志
我們都知道在Spring里的兩大核心模塊就是AOP和IOC欲鹏,其中AOP為面向切面編程,這是一種編程思想或者說范式臭墨,它并不是某一種語言所特有的語法赔嚎。
我們在開發(fā)業(yè)務(wù)代碼的時候,經(jīng)常有很多代碼是通用且重復(fù)的胧弛,這些代碼我們就可以作為一個切面提取出來尤误,放在一個切面類中,進行一個統(tǒng)一的處理结缚,這些處理就是指定在哪些切點織入哪些切面损晤。
例如,像日志記錄红竭,檢查用戶是否登錄尤勋,檢查用戶是否擁有管理員權(quán)限等十分通用且重復(fù)的功能代碼,就可以被作為一個切面提取出來茵宪。而框架中的AOP模塊最冰,可以幫助我們很方便的去實現(xiàn)AOP的編程方式,讓我們實現(xiàn)AOP更加簡單稀火。
本節(jié)將承接上一節(jié)暖哨,演示一下如何利用AOP實現(xiàn)簡單的http請求日志的記錄。首先創(chuàng)建一個切面類如下:
package org.zero01.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 static final Logger logger = LoggerFactory.getLogger(HttpAspect.class);
@Pointcut("execution(public * org.zero01.controller.StudentController.*(..))")
public void log() {
}
@Before("log()")
public void beforeLog(JoinPoint joinPoint) {
// 日志格式:url method clientIp classMethod param
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("url = {}", request.getRequestURL());
logger.info("method = {}", request.getMethod());
logger.info("clientIp = {}", request.getRemoteHost());
logger.info("class_method = {}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("param = {}", joinPoint.getArgs());
}
@AfterReturning(returning = "object", pointcut = "log()")
public void afterReturningLog(Object object) {
// 打印方法返回值內(nèi)容
logger.info("response = {}", object);
}
}
使用PostMan訪問方式如下:
訪問成功后凰狞,控制臺輸出日志如下:
如此篇裁,我們就完成了http請求日志的記錄。
封裝統(tǒng)一的返回數(shù)據(jù)對象
我們在控制器類的方法中服球,總是需要返回各種不同類型的數(shù)據(jù)給客戶端茴恰。例如,有時候需要返回集合對象斩熊、有時候返回字符串、有時候返回自定義對象等等伐庭。而且在一個方法里可能會因為處理的結(jié)果不同粉渠,而返回不同的對象。那么當(dāng)一個方法中需要根據(jù)不同的處理結(jié)果返回不同的對象時圾另,我們應(yīng)該怎么辦呢霸株?可能有人會想到把方法的返回類型設(shè)定為Object不就可以了,的確是可以集乔,但是這樣返回的數(shù)據(jù)格式就不統(tǒng)一去件。前端接收到數(shù)據(jù)時,很不方便去展示,后端寫接口文檔的時候也不好寫尤溜。所以我們應(yīng)該統(tǒng)一返回數(shù)據(jù)的格式倔叼,而使用Object就無法做到這一點了。
所以我們需要將返回的數(shù)據(jù)統(tǒng)一封裝在一個對象里宫莱,然后統(tǒng)一在控制器類的方法中丈攒,把這個對象設(shè)定為返回值類型即可,這樣我們返回的數(shù)據(jù)格式就有了一個標(biāo)準(zhǔn)授霸。那么我們就來開發(fā)一個這樣的對象吧巡验,首先新建一個枚舉類,因為我們需要把一些通用的常量數(shù)據(jù)都封裝在枚舉類里碘耳,以后這些數(shù)據(jù)發(fā)生變動時显设,只需要修改枚舉類即可。如果將這些常量數(shù)據(jù)硬編碼寫在代碼里就得逐個去修改了辛辨,十分的難以維護敷硅。代碼如下:
package org.zero01.enums;
public enum ResultEnum {
UNKONW_ERROR(-1, "未知錯誤"),
SUCCESS(0, "SUCCESS"),
ERROR(1, "ERROR"),
PRIMARY_SCHOOL(100, "小學(xué)生"),
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;
}
}
然后就是創(chuàng)建我們的返回數(shù)據(jù)封裝對象了,在此之前愉阎,我們需要先定義好這個數(shù)據(jù)的一個標(biāo)準(zhǔn)格式绞蹦。我這里定義的格式如下:
{
"code": 0,
"msg": "注冊成功",
"data": {
"sname": "Max",
"age": 18,
"sex": "woman",
"address": "湖南"
}
}
明確了數(shù)據(jù)的格式后,就可以開發(fā)我們的返回數(shù)據(jù)封裝對象了榜旦。新建一個類幽七,代碼如下:
package org.zero01.domain;
import org.zero01.enums.ResultEnum;
/**
* @program: sb-web
* @description: 服務(wù)器統(tǒng)一的返回數(shù)據(jù)封裝對象
* @author: 01
* @create: 2018-05-05 18:03
**/
public class Result<T> {
// 錯誤/正確碼
private Integer code;
// 提示信息
private String msg;
// 返回的數(shù)據(jù)
private T data;
private Result(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Result(Integer code) {
this.code = code;
}
private Result(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
private Result() {
}
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;
}
public static <T> Result<T> createBySuccessResultMessage(String msg) {
return new Result<T>(ResultEnum.SUCCESS.getCode(), msg);
}
public static <T> Result<T> createBySuccessCodeResult(Integer code, String msg) {
return new Result<T>(code, msg);
}
public static <T> Result<T> createBySuccessResult(String msg, T data) {
return new Result<T>(ResultEnum.SUCCESS.getCode(), msg, data);
}
public static <T> Result<T> createBySuccessResult() {
return new Result<T>(ResultEnum.SUCCESS.getCode());
}
public static <T> Result<T> createByErrorResult() {
return new Result<T>(ResultEnum.ERROR.getCode());
}
public static <T> Result<T> createByErrorResult(String msg, T data) {
return new Result<T>(ResultEnum.ERROR.getCode(), msg, data);
}
public static <T> Result<T> createByErrorCodeResult(Integer errorCode, String msg) {
return new Result<T>(errorCode, msg);
}
public static <T> Result<T> createByErrorResultMessage(String msg) {
return new Result<T>(ResultEnum.ERROR.getCode(), msg);
}
}
接著修改我們之前的注冊接口代碼如下:
@PostMapping("register.do")
public Result<Student> register(@Valid Student student, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return Result.createByErrorResultMessage(bindingResult.getFieldError().getDefaultMessage());
}
return Result.createBySuccessResult("注冊成功", student);
}
使用PostMan進行測試,數(shù)據(jù)正常的情況:
學(xué)生姓名為空的情況:
如上溅呢,可以看到澡屡,返回的數(shù)據(jù)格式都是一樣的,code字段的值用于判斷是一個success的結(jié)果還是一個error的結(jié)果咐旧,msg字段的值是提示信息碳褒,data字段則是存儲具體的數(shù)據(jù)。有這樣一個統(tǒng)一的格式后礁苗,前端也好解析這個json數(shù)據(jù)嗽上,我們后端在寫接口文檔的時候也好寫了。
統(tǒng)一異常處理
一個系統(tǒng)或應(yīng)用程序在運行的過程中伊约,由于種種因素姚淆,肯定是會有拋異常的情況的。在系統(tǒng)出現(xiàn)異常時屡律,由于服務(wù)的中斷腌逢,數(shù)據(jù)可能會得不到返回,亦或者返回的是一個與我們定義的數(shù)據(jù)格式不相符的一個數(shù)據(jù)超埋,這是我們不希望出現(xiàn)的問題搏讶。所以我們得進行一個全局統(tǒng)一的異常處理佳鳖,攔截系統(tǒng)中會出現(xiàn)的異常,并進行處理媒惕。下面我們用一個小例子來做為演示系吩。
例如,現(xiàn)在有一個業(yè)務(wù)需求如下:
- 獲取某學(xué)生的年齡進行判斷吓笙,小于10淑玫,拋出異常并返回“小學(xué)生”提示信息,大于10且小于16面睛,拋出異常并返回“初中生”提示信息絮蒿。
首先我們需要自定義一個異常,因為默認(rèn)的異常構(gòu)造器只接受一個字符串類型的數(shù)據(jù)叁鉴,而我們返回的數(shù)據(jù)中有一個code土涝,所以我們得自己定義個異常類。代碼如下:
package org.zero01.exception;
/**
* @program: sb-web
* @description: 自定義異常
* @author: 01
* @create: 2018-05-05 19:01
**/
public class StudentException extends RuntimeException {
private Integer code;
public StudentException(Integer code, String msg) {
super(msg);
this.code = code;
}
public Integer getCode() {
return code;
}
}
新建一個 ErrorHandler 類幌墓,用于全局異常的攔截及處理但壮。代碼如下:
package org.zero01.handle;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.zero01.domain.Result;
import org.zero01.enums.ResultEnum;
import org.zero01.exception.StudentException;
/**
* @program: sb-web
* @description: 全局異常處理類
* @author: 01
* @create: 2018-05-05 18:48
**/
// 定義全局異常處理類
@ControllerAdvice
// Lombok的一個注解,用于日志打印
@Slf4j
public class ErrorHandler {
// 聲明異常處理方法常侣,傳遞哪一個異常對象的class蜡饵,就代表該方法會攔截哪一個異常對象包括其子類
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Result exceptionHandle(Exception e) {
if (e instanceof StudentException) {
StudentException studentException = (StudentException) e;
// 返回統(tǒng)一的數(shù)據(jù)格式
return Result.createByErrorCodeResult(studentException.getCode(), studentException.getMessage());
}
// 打印異常日志
log.error("[系統(tǒng)異常]{}", e);
// 返回統(tǒng)一的數(shù)據(jù)格式
return Result.createByErrorCodeResult(ResultEnum.UNKONW_ERROR.getCode(), "服務(wù)器內(nèi)部出現(xiàn)未知錯誤");
}
}
注:我這里使用到了Lombok,如果對Lombok不熟悉的話胳施,可以參考我之前寫的一篇Lombok快速入門
在之前的控制類中溯祸,增加如下代碼:
@Autowired
private IStudentService iStudentService;
@GetMapping("check_age.do")
public void checkAge(Integer age) throws Exception {
iStudentService.checkAge(age);
age.toString();
}
我們都知道具體的邏輯都是寫在service層的,所以新建一個service包舞肆,在該包中新建一個接口焦辅。代碼如下:
package org.zero01.service;
public interface IStudentService {
void checkAge(Integer age) throws Exception;
}
然后新建一個類,實現(xiàn)該接口椿胯。代碼如下:
package org.zero01.service;
import org.springframework.stereotype.Service;
import org.zero01.enums.ResultEnum;
import org.zero01.exception.StudentException;
@Service("iStudentService")
public class StudentService implements IStudentService {
public void checkAge(Integer age) throws StudentException {
if (age < 10) {
throw new StudentException(ResultEnum.PRIMARY_SCHOOL.getCode(), ResultEnum.PRIMARY_SCHOOL.getMsg());
} else if (age > 10 && age < 16) {
throw new StudentException(ResultEnum.MIDDLE_SCHOOL.getCode(), ResultEnum.MIDDLE_SCHOOL.getMsg());
}
}
}
完成以上的代碼編寫后筷登,就可以開始進行測試了。age < 10
的情況:
age > 10 && age < 16
的情況:
age字段為空哩盲,出現(xiàn)系統(tǒng)異常的情況:
因為我們打印了日志前方,所以出現(xiàn)系統(tǒng)異常的時候也會輸出日志信息,不至于我們無法定位到異常:
從以上的測試結(jié)果中可以看到种冬,即便拋出了異常镣丑,我們返回的數(shù)據(jù)格式依舊是固定的,這樣就不會由于系統(tǒng)出現(xiàn)異常而返回不一樣的數(shù)據(jù)格式娱两。
單元測試
我們一般會在開發(fā)完項目中的某一個功能的時候,就會進行一個單元測試金吗。以確保交付項目時十兢,我們的代碼都是通過測試并且功能正常的趣竣,這是一個開發(fā)人員基本的素養(yǎng)。所以本節(jié)將簡單介紹service層的測試與controller層的測試方式旱物。
首先是service層的測試方式遥缕,service層的單元測試和我們平時寫的測試沒太大區(qū)別。在工程的test目錄下宵呛,新建一個測試類单匣,代碼如下:
package org.zero01;
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;
import org.zero01.domain.Result;
import org.zero01.domain.Student;
import org.zero01.service.IStudentService;
/**
* @program: sb-web
* @description: Student測試類
* @author: 01
* @create: 2018-05-05 21:46
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class StudentServiceTest {
@Autowired
private IStudentService iStudentService;
@Test
public void findOneTest() {
Result<Student> result = iStudentService.findOne(1);
Student student = result.getData();
Assert.assertEquals(18, student.getAge());
}
}
執(zhí)行該測試用例,運行結(jié)果如下:
我們修改一下年齡為15宝穗,以此模擬一下測試不通過的情況:
service層的測試比較簡單户秤,就介紹到這。接下來我們看一下controller層的測試方式逮矛。IDEA中有一個比較方便的功能可以幫我們生成測試方法鸡号,到需要被測試的controller類中,按 Ctrl + Shift + t 就可以快速創(chuàng)建測試方法须鼎。如下鲸伴,點擊Create New Test:
然后選擇需要測試的方法:
生成的測試用例代碼如下:
package org.zero01.controller;
import org.junit.Test;
import static org.junit.Assert.*;
public class StudentControllerTest {
@Test
public void checkAge() {
}
}
接著我們來完成這個測試代碼,controller層的測試和service層不太一樣晋控,因為需要訪問url汞窗,而不是直接調(diào)用方法進行測試。測試代碼如下:
package org.zero01.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;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class StudentControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void checkAge() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/check_age.do") // 使用get請求
.param("age","18")) // url參數(shù)
.andExpect(MockMvcResultMatchers.status().isOk()); // 判斷返回的狀態(tài)是否正常
}
}
運行該測試用例赡译,因為我們之前實現(xiàn)了一個記錄http訪問日志的功能仲吏,所以可以直接通過控制臺的輸出日志來判斷接口是否有被請求到:
單元測試就介紹到這,畢竟一般我們不會在代碼上測試controller層捶朵,而是使用postman或者restlet client等工具進行測試蜘矢。