初識SpringBoot Web開發
雖說前端的h5和js都可以完成表單的字段驗證,但是這只能是防止一些小白、誤操作而已。如果是一些別有用心的人,是很容易越過這些前端驗證的,有句話就是說永遠不要相信客戶端傳遞過來的數據。所以前端驗證之後,後端也需要再次進行表單字段的驗證,以確保數據到後端後是正確的、符合規範的。本節就簡單介紹一下,在SpringBoot的時候如何進行表單驗證。
首先創建一個SpringBoot工程,其中pom.xml配置文件主要配置內容如下:
<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>
創建一個pojo類,在該類中需要驗證的字段上加上驗證註解。代碼如下:
package org.zero01.domain; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; public class Student { @NotNull(message = "學生名字不能為空") private String sname; @Min(value = 18,message = "未成年禁止註冊") private int age; @NotNull(message = "性別不能為空") private String sex; @NotNull(message = "聯系地址不能為空") private String address; public String toString() { return "Student{" + "sname=‘" + sname + ‘\‘‘ + ", age=" + age + ", sex=‘" + sex + ‘\‘‘ + ", address=‘" + address + ‘\‘‘ + ‘}‘; } ... getter setter 略 ... }
創建一個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歲的情況:
控制臺打印結果:
未成年禁止註冊
非空字段為空的情況:
控制臺打印結果:
學生名字不能為空
使用AOP記錄請求日誌
我們都知道在Spring裏的兩大核心模塊就是AOP和IOC,其中AOP為面向切面編程,這是一種編程思想或者說範式,它並不是某一種語言所特有的語法。
我們在開發業務代碼的時候,經常有很多代碼是通用且重復的,這些代碼我們就可以作為一個切面提取出來,放在一個切面類中,進行一個統一的處理,這些處理就是指定在哪些切點織入哪些切面。
例如,像日誌記錄,檢查用戶是否登錄,檢查用戶是否擁有管理員權限等十分通用且重復的功能代碼,就可以被作為一個切面提取出來。而框架中的AOP模塊,可以幫助我們很方便的去實現AOP的編程方式,讓我們實現AOP更加簡單。
本節將承接上一節,演示一下如何利用AOP實現簡單的http請求日誌的記錄。首先創建一個切面類如下:
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) {
// 打印方法返回值內容
logger.info("response = {}", object);
}
}
使用PostMan訪問方式如下:
訪問成功後,控制臺輸出日誌如下:
如此,我們就完成了http請求日誌的記錄。
封裝統一的返回數據對象
我們在控制器類的方法中,總是需要返回各種不同類型的數據給客戶端。例如,有時候需要返回集合對象、有時候返回字符串、有時候返回自定義對象等等。而且在一個方法裏可能會因為處理的結果不同,而返回不同的對象。那麽當一個方法中需要根據不同的處理結果返回不同的對象時,我們應該怎麽辦呢?可能有人會想到把方法的返回類型設定為Object不就可以了,的確是可以,但是這樣返回的數據格式就不統一。前端接收到數據時,很不方便去展示,後端寫接口文檔的時候也不好寫。所以我們應該統一返回數據的格式,而使用Object就無法做到這一點了。
所以我們需要將返回的數據統一封裝在一個對象裏,然後統一在控制器類的方法中,把這個對象設定為返回值類型即可,這樣我們返回的數據格式就有了一個標準。那麽我們就來開發一個這樣的對象吧,首先新建一個枚舉類,因為我們需要把一些通用的常量數據都封裝在枚舉類裏,以後這些數據發生變動時,只需要修改枚舉類即可。如果將這些常量數據硬編碼寫在代碼裏就得逐個去修改了,十分的難以維護。代碼如下:
package org.zero01.enums;
public enum ResultEnum {
UNKONW_ERROR(-1, "未知錯誤"),
SUCCESS(0, "SUCCESS"),
ERROR(1, "ERROR"),
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;
}
}
然後就是創建我們的返回數據封裝對象了,在此之前,我們需要先定義好這個數據的一個標準格式。我這裏定義的格式如下:
{
"code": 0,
"msg": "註冊成功",
"data": {
"sname": "Max",
"age": 18,
"sex": "woman",
"address": "湖南"
}
}
明確了數據的格式後,就可以開發我們的返回數據封裝對象了。新建一個類,代碼如下:
package org.zero01.domain;
import org.zero01.enums.ResultEnum;
/**
* @program: sb-web
* @description: 服務器統一的返回數據封裝對象
* @author: 01
* @create: 2018-05-05 18:03
**/
public class Result<T> {
// 錯誤/正確碼
private Integer code;
// 提示信息
private String msg;
// 返回的數據
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進行測試,數據正常的情況:
學生姓名為空的情況:
如上,可以看到,返回的數據格式都是一樣的,code字段的值用於判斷是一個success的結果還是一個error的結果,msg字段的值是提示信息,data字段則是存儲具體的數據。有這樣一個統一的格式後,前端也好解析這個json數據,我們後端在寫接口文檔的時候也好寫了。
統一異常處理
一個系統或應用程序在運行的過程中,由於種種因素,肯定是會有拋異常的情況的。在系統出現異常時,由於服務的中斷,數據可能會得不到返回,亦或者返回的是一個與我們定義的數據格式不相符的一個數據,這是我們不希望出現的問題。所以我們得進行一個全局統一的異常處理,攔截系統中會出現的異常,並進行處理。下面我們用一個小例子來做為演示。
例如,現在有一個業務需求如下:
- 獲取某學生的年齡進行判斷,小於10,拋出異常並返回“小學生”提示信息,大於10且小於16,拋出異常並返回“初中生”提示信息。
首先我們需要自定義一個異常,因為默認的異常構造器只接受一個字符串類型的數據,而我們返回的數據中有一個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;
// 返回統一的數據格式
return Result.createByErrorCodeResult(studentException.getCode(), studentException.getMessage());
}
// 打印異常日誌
log.error("[系統異常]{}", e);
// 返回統一的數據格式
return Result.createByErrorCodeResult(ResultEnum.UNKONW_ERROR.getCode(), "服務器內部出現未知錯誤");
}
}
註:我這裏使用到了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;
}
然後新建一個類,實現該接口。代碼如下:
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字段為空,出現系統異常的情況:
因為我們打印了日誌,所以出現系統異常的時候也會輸出日誌信息,不至於我們無法定位到異常:
從以上的測試結果中可以看到,即便拋出了異常,我們返回的數據格式依舊是固定的,這樣就不會由於系統出現異常而返回不一樣的數據格式。
單元測試
我們一般會在開發完項目中的某一個功能的時候,就會進行一個單元測試。以確保交付項目時,我們的代碼都是通過測試並且功能正常的,這是一個開發人員基本的素養。所以本節將簡單介紹service層的測試與controller層的測試方式。
首先是service層的測試方式,service層的單元測試和我們平時寫的測試沒太大區別。在工程的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());
}
}
執行該測試用例,運行結果如下:
我們修改一下年齡為15,以此模擬一下測試不通過的情況:
service層的測試比較簡單,就介紹到這。接下來我們看一下controller層的測試方式。IDEA中有一個比較方便的功能可以幫我們生成測試方法,到需要被測試的controller類中,按 Ctrl + Shift + t 就可以快速創建測試方法。如下,點擊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,而不是直接調用方法進行測試。測試代碼如下:
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參數
.andExpect(MockMvcResultMatchers.status().isOk()); // 判斷返回的狀態是否正常
}
}
運行該測試用例,因為我們之前實現了一個記錄http訪問日誌的功能,所以可以直接通過控制臺的輸出日誌來判斷接口是否有被請求到:
單元測試就介紹到這,畢竟一般我們不會在代碼上測試controller層,而是使用postman或者restlet client等工具進行測試。
初識SpringBoot Web開發