使用验证注解来实现表单验证
虽说前端的h5和js都可以完成表单的字段验证,但是这只能是防止一些小白、误操作而已。如果是一些别有用心的人,是很容易越过这些前端验证的,有句话就是说永远不要相信客户端传递过来的数据。所以前端验证之后,后端也需要再次进行表单字段的验证,以确保数据到后端后是正确的、符合规范的。本节就简单介绍一下,在SpringBoot的时候如何进行表单验证。
首先创建一个SpringBoot工程,其中pom.xml配置文件主要配置内容如下:
代码语言:javascript复制 <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类,在该类中需要验证的字段上加上验证注解。代码如下:
代码语言:javascript复制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类:
代码语言:javascript复制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;
}
}
启动运行类,代码如下:
代码语言:javascript复制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岁的情况:
控制台打印结果:
代码语言:javascript复制未成年禁止注册
非空字段为空的情况:
控制台打印结果:
代码语言:javascript复制学生名字不能为空
使用AOP记录请求日志
我们都知道在Spring里的两大核心模块就是AOP和IOC,其中AOP为面向切面编程,这是一种编程思想或者说范式,它并不是某一种语言所特有的语法。
我们在开发业务代码的时候,经常有很多代码是通用且重复的,这些代码我们就可以作为一个切面提取出来,放在一个切面类中,进行一个统一的处理,这些处理就是指定在哪些切点织入哪些切面。
例如,像日志记录,检查用户是否登录,检查用户是否拥有管理员权限等十分通用且重复的功能代码,就可以被作为一个切面提取出来。而框架中的AOP模块,可以帮助我们很方便的去实现AOP的编程方式,让我们实现AOP更加简单。
本节将承接上一节,演示一下如何利用AOP实现简单的http请求日志的记录。首先创建一个切面类如下:
代码语言:javascript复制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就无法做到这一点了。
所以我们需要将返回的数据统一封装在一个对象里,然后统一在控制器类的方法中,把这个对象设定为返回值类型即可,这样我们返回的数据格式就有了一个标准。那么我们就来开发一个这样的对象吧,首先新建一个枚举类,因为我们需要把一些通用的常量数据都封装在枚举类里,以后这些数据发生变动时,只需要修改枚举类即可。如果将这些常量数据硬编码写在代码里就得逐个去修改了,十分的难以维护。代码如下:
代码语言:javascript复制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;
}
}
然后就是创建我们的返回数据封装对象了,在此之前,我们需要先定义好这个数据的一个标准格式。我这里定义的格式如下:
代码语言:javascript复制{
"code": 0,
"msg": "注册成功",
"data": {
"sname": "Max",
"age": 18,
"sex": "woman",
"address": "湖南"
}
}
明确了数据的格式后,就可以开发我们的返回数据封装对象了。新建一个类,代码如下:
代码语言:javascript复制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> createBySucce***esultMessage(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> createBySucce***esult(String msg, T data) {
return new Result<T>(ResultEnum.SUCCESS.getCode(), msg, data);
}
public static <T> Result<T> createBySucce***esult() {
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);
}
}
接着修改我们之前的注册接口代码如下:
代码语言:javascript复制@PostMapping("register.do")
public Result<Student> register(@Valid Student student, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return Result.createByErrorResultMessage(bindingResult.getFieldError().getDefaultMessage());
}
return Result.createBySucce***esult("注册成功", student);
}
使用PostMan进行测试,数据正常的情况:
学生姓名为空的情况:
如上,可以看到,返回的数据格式都是一样的,code字段的值用于判断是一个success的结果还是一个error的结果,msg字段的值是提示信息,data字段则是存储具体的数据。有这样一个统一的格式后,前端也好解析这个json数据,我们后端在写接口文档的时候也好写了。
统一异常处理
一个系统或应用程序在运行的过程中,由于种种因素,肯定是会有抛异常的情况的。在系统出现异常时,由于服务的中断,数据可能会得不到返回,亦或者返回的是一个与我们定义的数据格式不相符的一个数据,这是我们不希望出现的问题。所以我们得进行一个全局统一的异常处理,拦截系统中会出现的异常,并进行处理。下面我们用一个小例子来做为演示。
例如,现在有一个业务需求如下:
- 获取某学生的年龄进行判断,小于10,抛出异常并返回“小学生”提示信息,大于10且小于16,抛出异常并返回“初中生”提示信息。
首先我们需要自定义一个异常,因为默认的异常构造器只接受一个字符串类型的数据,而我们返回的数据中有一个code,所以我们得自己定义个异常类。代码如下:
代码语言:javascript复制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 类,用于全局异常的拦截及处理。代码如下:
代码语言:javascript复制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快速入门
在之前的控制类中,增加如下代码:
代码语言:javascript复制@Autowired
private IStudentService iStudentService;
@GetMapping("check_age.do")
public void checkAge(Integer age) throws Exception {
iStudentService.checkAge(age);
age.toString();
}
我们都知道具体的逻辑都是写在service层的,所以新建一个service包,在该包中新建一个接口。代码如下:
代码语言:javascript复制package org.zero01.service;
public interface IStudentService {
void checkAge(Integer age) throws Exception;
}
然后新建一个类,实现该接口。代码如下:
代码语言:javascript复制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目录下,新建一个测试类,代码如下:
代码语言:javascript复制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:
然后选择需要测试的方法:
生成的测试用例代码如下:
代码语言:javascript复制package org.zero01.controller;
import org.junit.Test;
import static org.junit.Assert.*;
public class StudentControllerTest {
@Test
public void checkAge() {
}
}
接着我们来完成这个测试代码,controller层的测试和service层不太一样,因为需要访问url,而不是直接调用方法进行测试。测试代码如下:
代码语言:javascript复制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等工具进行测试。